diff --git a/components/display/component.mk b/components/display/display/component.mk similarity index 77% rename from components/display/component.mk rename to components/display/display/component.mk index 87a22b4e..996021a1 100644 --- a/components/display/component.mk +++ b/components/display/display/component.mk @@ -6,7 +6,8 @@ # lib(subdirectory_name).a in the build directory. This behaviour is entirely configurable, # please read the SDK documents if you need to do this. # -COMPONENT_SRCDIRS := . tarablessd1306 +CFLAGS += -I$(COMPONENT_PATH)/../squeezelite +COMPONENT_SRCDIRS := . tarablessd1306 tarablessd1306/fonts tarablessd1306/ifaces COMPONENT_ADD_INCLUDEDIRS := . COMPONENT_ADD_INCLUDEDIRS += ./tarablessd1306 diff --git a/components/display/display/display.c b/components/display/display/display.c new file mode 100644 index 00000000..831e1d06 --- /dev/null +++ b/components/display/display/display.c @@ -0,0 +1,76 @@ +/* + * (c) 2004,2006 Richard Titmuss for SlimProtoLib + * (c) Philippe G. 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include +#include "esp_log.h" +#include "config.h" +#include "embedded.h" +#include "display.h" + +#define TAG "display" + +static bool (*slimp_handler_chain)(u8_t *data, int len); +static struct display_handle_s *handle; + +static bool display_handler(u8_t *data, int len); + +/**************************************************************************************** + * + */ +void display_init(void) { + char *item = config_alloc_get(NVS_TYPE_STR, "display_config"); + + if (item && *item) { + handle = &SSD1306_handle; + if (handle->init(item)) { + slimp_handler_chain = slimp_handler; + slimp_handler = display_handler; + ESP_LOGI(TAG, "Display initialization successful"); + } else { + ESP_LOGI(TAG, "Display initialization failed"); + } + } else { + ESP_LOGI(TAG, "no display"); + } + + if (item) free(item); +} + +/**************************************************************************************** + * Process graphic display data + */ +static bool display_handler(u8_t *data, int len){ + bool res = true; + + if (!strncmp((char*) data, "vfdc", 4)) { + handle->vfdc_handler(data, len); + } else if (!strncmp((char*) data, "grfe", 4)) { + handle->grfe_handler(data, len); + } else { + res = false; + } + + // chain protocol handlers (bitwise or is fine) + if (*slimp_handler_chain) res |= (*slimp_handler_chain)(data, len); + return res; +} + diff --git a/components/display/display/display.h b/components/display/display/display.h new file mode 100644 index 00000000..058121da --- /dev/null +++ b/components/display/display/display.h @@ -0,0 +1,32 @@ +/* + * (c) Philippe G. 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +struct display_handle_s { + bool (*init)(char *config); + void (*vfdc_handler)(u8_t *data, int len); + void (*grfe_handler)(u8_t *data, int len); + void (*grfb_handler)(u8_t *data, int len); + void (*visu_handler)(u8_t *data, int len); +}; + +extern struct display_handle_s SSD1306_handle; + + + diff --git a/components/display/display/driver_SSD1306.c b/components/display/display/driver_SSD1306.c new file mode 100644 index 00000000..cdcc72ef --- /dev/null +++ b/components/display/display/driver_SSD1306.c @@ -0,0 +1,245 @@ +/* + * (c) 2004,2006 Richard Titmuss for SlimProtoLib + * (c) Philippe G. 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include +#include "esp_log.h" +#include "display.h" + +#include "ssd1306.h" +#include "ssd1306_draw.h" +#include "ssd1306_font.h" +#include "ssd1306_default_if.h" + +#define I2C_PORT 1 +#define I2C_ADDRESS 0x3C +#define LINELEN 40 +#define TAG "display" + +static void vfdc_handler( u8_t *_data, int bytes_read); +void grfe_handler( u8_t *data, int len); +static bool display_init(char *config); + +struct display_handle_s SSD1306_handle = { + display_init, + vfdc_handler, + grfe_handler, + NULL, NULL, +}; + +static struct SSD1306_Device I2CDisplay; +static SSD1306_AddressMode AddressMode = AddressMode_Invalid; + +static const unsigned char BitReverseTable256[] = +{ + 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, + 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, + 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, + 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, + 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, + 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, + 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, + 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, + 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, + 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, + 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, + 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, + 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, + 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, + 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, + 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF +}; + +/**************************************************************************************** + * + */ +static bool display_init(char *config) { + bool res = false; + + if (strstr(config, "I2C")) { + int scl = -1, sda = -1; + int width = -1, height = -1; + char *p; + + // no time for smart parsing - this is for tinkerers + if ((p = strcasestr(config, "scl")) != NULL) scl = atoi(strchr(p, '=') + 1); + if ((p = strcasestr(config, "sda")) != NULL) sda = atoi(strchr(p, '=') + 1); + if ((p = strcasestr(config, "width")) != NULL) width = atoi(strchr(p, '=') + 1); + if ((p = strcasestr(config, "height")) != NULL) height = atoi(strchr(p, '=') + 1); + + if (sda != -1 && scl != -1 && width != -1 && height != -1) { + SSD1306_I2CMasterInitDefault( I2C_PORT, sda, scl ); + SSD1306_I2CMasterAttachDisplayDefault( &I2CDisplay, width, height, I2C_ADDRESS, -1); + SSD1306_SetFont( &I2CDisplay, &Font_droid_sans_fallback_15x17 ); + ESP_LOGI(TAG, "Initialized I2C display %dx%d (sda:%d, scl:%d)", width, height, sda, scl); + res = true; + } else { + ESP_LOGI(TAG, "Cannot initialized I2C display %s [%dx%d sda:%d, scl:%d]", config, width, height, sda, scl); + } + } else { + // other types + } + + return res; +} + +/**************************************************************************************** + * Change special LCD chars to something more printable on screen + */ +static void makeprintable(unsigned char * line) { + for (int n = 0; n < LINELEN; n++) { + switch (line[n]) { + case 11: /* block */ + line[n] = '#'; + break;; + case 16: /* rightarrow */ + line[n] = '>'; + break;; + case 22: /* circle */ + line[n] = '@'; + break;; + case 145: /* note */ + line[n] = ' '; + break;; + case 152: /* bell */ + line[n] = 'o'; + break; + default: + break; + } + } +} + +/**************************************************************************************** + * Check if char is printable, or a valid symbol + */ +static bool charisok(unsigned char c) { + switch (c) { + case 11: /* block */ + case 16: /* rightarrow */ + case 22: /* circle */ + case 145: /* note */ + case 152: /* bell */ + return true; + break;; + default: + return isprint(c); + } +} + +/**************************************************************************************** + * Show the display (text mode) + */ +static void show_display_buffer(char *ddram) { + char line1[LINELEN+1]; + char *line2; + + memset(line1, 0, LINELEN+1); + strncpy(line1, ddram, LINELEN); + line2 = &(ddram[LINELEN]); + line2[LINELEN] = '\0'; + + /* Convert special LCD chars */ + makeprintable((unsigned char *)line1); + makeprintable((unsigned char *)line2); + + ESP_LOGI(TAG, "\n\t%.40s\n\t%.40s", line1, line2); + + SSD1306_Clear( &I2CDisplay, SSD_COLOR_BLACK ); + SSD1306_FontDrawAnchoredString( &I2CDisplay, TextAnchor_NorthWest, line1, SSD_COLOR_WHITE ); + SSD1306_FontDrawAnchoredString( &I2CDisplay, TextAnchor_SouthWest, line2, SSD_COLOR_WHITE ); + + // check addressing mode by rows + if (AddressMode != AddressMode_Horizontal) { + AddressMode = AddressMode_Horizontal; + SSD1306_SetDisplayAddressMode( &I2CDisplay, AddressMode ); + } + + SSD1306_Update( &I2CDisplay ); +} + +/**************************************************************************************** + * Process display data + */ +static void vfdc_handler( u8_t *_data, int bytes_read) { + unsigned short *data = (unsigned short*) _data, *display_data; + char ddram[(LINELEN + 1) * 2]; + int n, addr = 0; /* counter */ + + bytes_read -= 4; + if (bytes_read % 2) bytes_read--; /* even number of bytes */ + // if we use Noritake VFD codes, display data starts at 12 + display_data = &(data[5]); /* display data starts at byte 10 */ + + memset(ddram, ' ', LINELEN * 2); + + for (n = 0; n < (bytes_read/2); n++) { + unsigned short d; /* data element */ + unsigned char t, c; + + d = ntohs(display_data[n]); + t = (d & 0x00ff00) >> 8; /* type of display data */ + c = (d & 0x0000ff); /* character/command */ + switch (t) { + case 0x03: /* character */ + if (!charisok(c)) c = ' '; + if (addr <= LINELEN * 2) { + ddram[addr++] = c; + } + break; + case 0x02: /* command */ + switch (c) { + case 0x06: /* display clear */ + memset(ddram, ' ', LINELEN * 2); + break; + case 0x02: /* cursor home */ + addr = 0; + break; + case 0xc0: /* cursor home2 */ + addr = LINELEN; + break; + } + } + } + + show_display_buffer(ddram); +} + +/**************************************************************************************** + * Process graphic display data + */ +void grfe_handler( u8_t *data, int len) { + data += 8; + len -= 8; + + // to be verified, but this is as fast as using a pointer on data + for (int i = len - 1; i >= 0; i--) data[i] = BitReverseTable256[data[i]]; + + // check addressing mode by columns + if (AddressMode != AddressMode_Vertical) { + AddressMode = AddressMode_Vertical; + SSD1306_SetDisplayAddressMode( &I2CDisplay, AddressMode ); + } + + SSD1306_WriteRawData( &I2CDisplay, data, len); +} + + diff --git a/components/display/tarablessd1306/.gitignore b/components/display/display/tarablessd1306/.gitignore similarity index 100% rename from components/display/tarablessd1306/.gitignore rename to components/display/display/tarablessd1306/.gitignore diff --git a/components/display/tarablessd1306/CMakeLists.txt b/components/display/display/tarablessd1306/CMakeLists.txt similarity index 100% rename from components/display/tarablessd1306/CMakeLists.txt rename to components/display/display/tarablessd1306/CMakeLists.txt diff --git a/components/display/tarablessd1306/Kconfig b/components/display/display/tarablessd1306/Kconfig similarity index 100% rename from components/display/tarablessd1306/Kconfig rename to components/display/display/tarablessd1306/Kconfig diff --git a/components/display/tarablessd1306/LICENSE b/components/display/display/tarablessd1306/LICENSE similarity index 100% rename from components/display/tarablessd1306/LICENSE rename to components/display/display/tarablessd1306/LICENSE diff --git a/components/display/tarablessd1306/README.md b/components/display/display/tarablessd1306/README.md similarity index 100% rename from components/display/tarablessd1306/README.md rename to components/display/display/tarablessd1306/README.md diff --git a/components/display/tarablessd1306/component.mk b/components/display/display/tarablessd1306/component.mk similarity index 100% rename from components/display/tarablessd1306/component.mk rename to components/display/display/tarablessd1306/component.mk diff --git a/components/display/tarablessd1306/fonts/LICENSE-apache b/components/display/display/tarablessd1306/fonts/LICENSE-apache similarity index 100% rename from components/display/tarablessd1306/fonts/LICENSE-apache rename to components/display/display/tarablessd1306/fonts/LICENSE-apache diff --git a/components/display/tarablessd1306/fonts/LICENSE-liberation-mono b/components/display/display/tarablessd1306/fonts/LICENSE-liberation-mono similarity index 100% rename from components/display/tarablessd1306/fonts/LICENSE-liberation-mono rename to components/display/display/tarablessd1306/fonts/LICENSE-liberation-mono diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_fallback_11x13.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_11x13.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_fallback_11x13.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_11x13.c diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_fallback_15x17.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_15x17.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_fallback_15x17.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_15x17.c diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_fallback_24x28.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_24x28.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_fallback_24x28.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_fallback_24x28.c diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_mono_13x24.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_mono_13x24.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_mono_13x24.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_mono_13x24.c diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_mono_16x31.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_mono_16x31.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_mono_16x31.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_mono_16x31.c diff --git a/components/display/tarablessd1306/fonts/font_droid_sans_mono_7x13.c b/components/display/display/tarablessd1306/fonts/font_droid_sans_mono_7x13.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_droid_sans_mono_7x13.c rename to components/display/display/tarablessd1306/fonts/font_droid_sans_mono_7x13.c diff --git a/components/display/tarablessd1306/fonts/font_liberation_mono_13x21.c b/components/display/display/tarablessd1306/fonts/font_liberation_mono_13x21.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_liberation_mono_13x21.c rename to components/display/display/tarablessd1306/fonts/font_liberation_mono_13x21.c diff --git a/components/display/tarablessd1306/fonts/font_liberation_mono_17x30.c b/components/display/display/tarablessd1306/fonts/font_liberation_mono_17x30.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_liberation_mono_17x30.c rename to components/display/display/tarablessd1306/fonts/font_liberation_mono_17x30.c diff --git a/components/display/tarablessd1306/fonts/font_liberation_mono_9x15.c b/components/display/display/tarablessd1306/fonts/font_liberation_mono_9x15.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_liberation_mono_9x15.c rename to components/display/display/tarablessd1306/fonts/font_liberation_mono_9x15.c diff --git a/components/display/tarablessd1306/fonts/font_tarable7seg_16x32.c b/components/display/display/tarablessd1306/fonts/font_tarable7seg_16x32.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_tarable7seg_16x32.c rename to components/display/display/tarablessd1306/fonts/font_tarable7seg_16x32.c diff --git a/components/display/tarablessd1306/fonts/font_tarable7seg_32x64.c b/components/display/display/tarablessd1306/fonts/font_tarable7seg_32x64.c similarity index 100% rename from components/display/tarablessd1306/fonts/font_tarable7seg_32x64.c rename to components/display/display/tarablessd1306/fonts/font_tarable7seg_32x64.c diff --git a/components/display/tarablessd1306/ifaces/default_if_i2c.c b/components/display/display/tarablessd1306/ifaces/default_if_i2c.c similarity index 100% rename from components/display/tarablessd1306/ifaces/default_if_i2c.c rename to components/display/display/tarablessd1306/ifaces/default_if_i2c.c diff --git a/components/display/tarablessd1306/ifaces/default_if_spi.c b/components/display/display/tarablessd1306/ifaces/default_if_spi.c similarity index 100% rename from components/display/tarablessd1306/ifaces/default_if_spi.c rename to components/display/display/tarablessd1306/ifaces/default_if_spi.c diff --git a/components/display/tarablessd1306/ssd1306.c b/components/display/display/tarablessd1306/ssd1306.c similarity index 99% rename from components/display/tarablessd1306/ssd1306.c rename to components/display/display/tarablessd1306/ssd1306.c index 3d88e171..c2ef7865 100644 --- a/components/display/tarablessd1306/ssd1306.c +++ b/components/display/display/tarablessd1306/ssd1306.c @@ -225,7 +225,7 @@ static bool SSD1306_Init( struct SSD1306_Device* DeviceHandle, int Width, int He SSD1306_SetInverted( DeviceHandle, false ); SSD1306_SetDisplayClocks( DeviceHandle, 0, 8 ); EnableChargePumpRegulator( DeviceHandle ); - SSD1306_SetDisplayAddressMode( DeviceHandle, AddressMode_Horizontal ); + SSD1306_SetDisplayAddressMode( DeviceHandle, AddressMode_Vertical ); SSD1306_SetColumnAddress( DeviceHandle, 0, DeviceHandle->Width - 1 ); SSD1306_SetPageAddress( DeviceHandle, 0, ( DeviceHandle->Height / 8 ) - 1 ); SSD1306_EnableDisplayRAM( DeviceHandle ); diff --git a/components/display/tarablessd1306/ssd1306.h b/components/display/display/tarablessd1306/ssd1306.h similarity index 100% rename from components/display/tarablessd1306/ssd1306.h rename to components/display/display/tarablessd1306/ssd1306.h diff --git a/components/display/tarablessd1306/ssd1306_default_if.h b/components/display/display/tarablessd1306/ssd1306_default_if.h similarity index 100% rename from components/display/tarablessd1306/ssd1306_default_if.h rename to components/display/display/tarablessd1306/ssd1306_default_if.h diff --git a/components/display/tarablessd1306/ssd1306_draw.c b/components/display/display/tarablessd1306/ssd1306_draw.c similarity index 100% rename from components/display/tarablessd1306/ssd1306_draw.c rename to components/display/display/tarablessd1306/ssd1306_draw.c diff --git a/components/display/tarablessd1306/ssd1306_draw.h b/components/display/display/tarablessd1306/ssd1306_draw.h similarity index 100% rename from components/display/tarablessd1306/ssd1306_draw.h rename to components/display/display/tarablessd1306/ssd1306_draw.h diff --git a/components/display/tarablessd1306/ssd1306_err.h b/components/display/display/tarablessd1306/ssd1306_err.h similarity index 100% rename from components/display/tarablessd1306/ssd1306_err.h rename to components/display/display/tarablessd1306/ssd1306_err.h diff --git a/components/display/tarablessd1306/ssd1306_font.c b/components/display/display/tarablessd1306/ssd1306_font.c similarity index 100% rename from components/display/tarablessd1306/ssd1306_font.c rename to components/display/display/tarablessd1306/ssd1306_font.c diff --git a/components/display/tarablessd1306/ssd1306_font.h b/components/display/display/tarablessd1306/ssd1306_font.h similarity index 100% rename from components/display/tarablessd1306/ssd1306_font.h rename to components/display/display/tarablessd1306/ssd1306_font.h diff --git a/components/display/text.c b/components/display/text.c deleted file mode 100644 index d850fe22..00000000 --- a/components/display/text.c +++ /dev/null @@ -1,187 +0,0 @@ -/* - * (c) 2004,2006 Richard Titmuss for SlimProtoLib - * (c) Philippe G. 2019, philippe_44@outlook.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include -#include -#include -#include "esp_log.h" -#include "config.h" - -#include "ssd1306.h" -#include "ssd1306_draw.h" -#include "ssd1306_font.h" -#include "ssd1306_default_if.h" - -#define LINELEN 40 -#define I2C_PORT 1 -#define I2C_ADDRESS 0x3C -#define TAG "display" - -static struct SSD1306_Device I2CDisplay; -static bool init, display; - -void display_init(void) { - char *item = config_alloc_get(NVS_TYPE_STR, "display_config"); - - if (!item || !*item) { - ESP_LOGI(TAG, "no display"); - return; - } - - if (strstr(item, "I2C")) { - int scl = -1, sda = -1; - int width = -1, height = -1; - char *p; - - // done no matter what - init = true; - - // no time for smart parsing - this is for tinkerers - if ((p = strcasestr(item, "scl")) != NULL) scl = atoi(strchr(p, '=') + 1); - if ((p = strcasestr(item, "sda")) != NULL) sda = atoi(strchr(p, '=') + 1); - if ((p = strcasestr(item, "width")) != NULL) width = atoi(strchr(p, '=') + 1); - if ((p = strcasestr(item, "height")) != NULL) height = atoi(strchr(p, '=') + 1); - - if (sda != -1 && scl != -1 && width != -1 && height != -1) { - SSD1306_I2CMasterInitDefault( I2C_PORT, sda, scl ); - SSD1306_I2CMasterAttachDisplayDefault( &I2CDisplay, width, height, I2C_ADDRESS, -1); - SSD1306_SetFont( &I2CDisplay, &Font_droid_sans_fallback_15x17 ); - ESP_LOGI(TAG, "initialized I2C display %dx%d (sda:%d, scl:%d)", width, height, sda, scl); - display = true; - } else { - ESP_LOGI(TAG, "cannot initialized I2C display %s [%dx%d sda:%d, scl:%d]", item, width, height, sda, scl); - } - } else { - // other types - } -} - -//Change special LCD chars to something more printable on screen -unsigned char printable(unsigned char c) { - switch (c) { - case 11: /* block */ - return '#'; - break;; - case 16: /* rightarrow */ - return '>'; - break;; - case 22: /* circle */ - return '@'; - break;; - case 145: /* note */ - return ' '; - break;; - case 152: /* bell */ - return 'o'; - break; - default: - return c; - } -} - -// Replace unprintable symbols in line -void makeprintable(unsigned char * line) { - for (int n = 0; n < LINELEN; n++) line[n] = printable(line[n]); -} - -// Show the display -void show_display_buffer(char *ddram) { - char line1[LINELEN+1]; - char *line2; - - memset(line1, 0, LINELEN+1); - strncpy(line1, ddram, LINELEN); - line2 = &(ddram[LINELEN]); - line2[LINELEN] = '\0'; - - /* Convert special LCD chars */ - makeprintable((unsigned char *)line1); - makeprintable((unsigned char *)line2); - - ESP_LOGI(TAG, "\n\t%.40s\n\t%.40s", line1, line2); - - if (display) { - SSD1306_Clear( &I2CDisplay, SSD_COLOR_BLACK ); - SSD1306_FontDrawAnchoredString( &I2CDisplay, TextAnchor_NorthWest, line1, SSD_COLOR_WHITE ); - SSD1306_FontDrawAnchoredString( &I2CDisplay, TextAnchor_SouthWest, line2, SSD_COLOR_WHITE ); - SSD1306_Update( &I2CDisplay ); - } -} - -// Check if char is printable, or a valid symbol -bool charisok(unsigned char c) { - switch (c) { - case 11: /* block */ - case 16: /* rightarrow */ - case 22: /* circle */ - case 145: /* note */ - case 152: /* bell */ - return true; - break;; - default: - return isprint(c); - } -} - -// Process display data -void vfd_data( unsigned short *data, int bytes_read) { - unsigned short *display_data; - char ddram[LINELEN * 2]; - int n; - int addr = 0; /* counter */ - - if (!init) display_init(); - - if (bytes_read % 2) bytes_read--; /* even number of bytes */ - // if we use Noritake VFD codes, display data starts at 12 - display_data = &(data[5]); /* display data starts at byte 10 */ - - memset(ddram, ' ', LINELEN * 2); - - for (n = 0; n < (bytes_read/2); n++) { - unsigned short d; /* data element */ - unsigned char t, c; - - d = ntohs(display_data[n]); - t = (d & 0x00ff00) >> 8; /* type of display data */ - c = (d & 0x0000ff); /* character/command */ - switch (t) { - case 0x03: /* character */ - if (!charisok(c)) c = ' '; - if (addr <= LINELEN * 2) { - ddram[addr++] = c; - } - break; - case 0x02: /* command */ - switch (c) { - case 0x06: /* display clear */ - memset(ddram, ' ', LINELEN * 2); - break; - case 0x02: /* cursor home */ - addr = 0; - break; - case 0xc0: /* cursor home2 */ - addr = LINELEN; - break; - } - } - } - - show_display_buffer(ddram); -} diff --git a/components/raop/raop.c b/components/raop/raop.c index 145ff0c2..69cccc26 100644 --- a/components/raop/raop.c +++ b/components/raop/raop.c @@ -1,954 +1,954 @@ -/* - * - * (c) Philippe 2019, philippe_44@outlook.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include - -#include "platform.h" - -#ifdef WIN32 -#include -#include -#include -#include -#include -#include "mdns.h" -#include "mdnsd.h" -#include "mdnssd-itf.h" -#else -#include "esp_pthread.h" -#include "mdns.h" -#include "mbedtls/version.h" -#include -#endif - -#include "util.h" -#include "raop.h" -#include "rtp.h" -#include "dmap_parser.h" -#include "log_util.h" - -#define RTSP_STACK_SIZE (8*1024) -#define SEARCH_STACK_SIZE (2*1048) - -typedef struct raop_ctx_s { -#ifdef WIN32 - struct mdns_service *svc; - struct mdnsd *svr; -#endif - struct in_addr host; // IP of bridge - short unsigned port; // RTSP port for AirPlay - int sock; // socket of the above - struct in_addr peer; // IP of the iDevice (airplay sender) - bool running; -#ifdef WIN32 - pthread_t thread, search_thread; -#else - TaskHandle_t thread, search_thread, joiner; - StaticTask_t *xTaskBuffer; - StackType_t xStack[RTSP_STACK_SIZE] __attribute__ ((aligned (4))); -#endif - unsigned char mac[6]; - int latency; - struct { - char *aesiv, *aeskey; - char *fmtp; - } rtsp; - struct rtp_s *rtp; - raop_cmd_cb_t cmd_cb; - raop_data_cb_t data_cb; - struct { - char DACPid[32], id[32]; - struct in_addr host; - u16_t port; -#ifdef WIN32 - struct mDNShandle_s *handle; -#else - bool running; - TaskHandle_t thread, joiner; - StaticTask_t *xTaskBuffer; - StackType_t xStack[SEARCH_STACK_SIZE] __attribute__ ((aligned (4)));; -#endif - } active_remote; - void *owner; -} raop_ctx_t; - -extern struct mdnsd* glmDNSServer; -extern log_level raop_loglevel; -static log_level *loglevel = &raop_loglevel; - -static void* rtsp_thread(void *arg); -static bool handle_rtsp(raop_ctx_t *ctx, int sock); - -static char* rsa_apply(unsigned char *input, int inlen, int *outlen, int mode); -static int base64_pad(char *src, char **padded); -static int base64_encode(const void *data, int size, char **str); -static int base64_decode(const char *str, void *data); -static void* search_remote(void *args); - -extern char private_key[]; - enum { RSA_MODE_KEY, RSA_MODE_AUTH }; - -static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len); - -/*----------------------------------------------------------------------------*/ -struct raop_ctx_s *raop_create(struct in_addr host, char *name, - unsigned char mac[6], int latency, - raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) { - struct raop_ctx_s *ctx = malloc(sizeof(struct raop_ctx_s)); - struct sockaddr_in addr; - char id[64]; - #ifdef WIN32 - socklen_t nlen = sizeof(struct sockaddr); - char *txt[] = { "am=airesp32", "tp=UDP", "sm=false", "sv=false", "ek=1", - "et=0,1", "md=0,1,2", "cn=0,1", "ch=2", - "ss=16", "sr=44100", "vn=3", "txtvers=1", - NULL }; -#else - mdns_txt_item_t txt[] = { - {"am", "airesp32"}, - {"tp", "UDP"}, - {"sm","false"}, - {"sv","false"}, - {"ek","1"}, - {"et","0,1"}, - {"md","0,1,2"}, - {"cn","0,1"}, - {"ch","2"}, - {"ss","16"}, - {"sr","44100"}, - {"vn","3"}, - {"txtvers","1"}, - }; - -#endif - - if (!ctx) return NULL; - - // make sure we have a clean context - memset(ctx, 0, sizeof(raop_ctx_t)); - -#ifdef WIN32 - ctx->svr = glmDNSServer; -#endif - ctx->host = host; - ctx->sock = socket(AF_INET, SOCK_STREAM, 0); - ctx->cmd_cb = cmd_cb; - ctx->data_cb = data_cb; - ctx->latency = min(latency, 88200); - if (ctx->sock == -1) { - LOG_ERROR("Cannot create listening socket", NULL); - free(ctx); - return NULL; - } - - memset(&addr, 0, sizeof(addr)); - addr.sin_addr.s_addr = host.s_addr; - addr.sin_family = AF_INET; -#ifdef WIN32 - ctx->port = 0; - addr.sin_port = htons(ctx->port); -#else - ctx->port = 5000; - addr.sin_port = htons(ctx->port); -#endif - - if (bind(ctx->sock, (struct sockaddr *) &addr, sizeof(addr)) < 0 || listen(ctx->sock, 1)) { - LOG_ERROR("Cannot bind or listen RTSP listener: %s", strerror(errno)); - free(ctx); - closesocket(ctx->sock); - return NULL; - } - -#ifdef WIN32 - getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); - ctx->port = ntohs(addr.sin_port); -#endif - ctx->running = true; - memcpy(ctx->mac, mac, 6); - snprintf(id, 64, "%02X%02X%02X%02X%02X%02X@%s", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], name); - #ifdef WIN32 - // seems that Windows snprintf does not add NULL char if actual size > max - id[63] = '\0'; - ctx->svc = mdnsd_register_svc(ctx->svr, id, "_raop._tcp.local", ctx->port, NULL, (const char**) txt); - pthread_create(&ctx->thread, NULL, &rtsp_thread, ctx); - #else - LOG_INFO("starting mDNS with %s", id); - ESP_ERROR_CHECK( mdns_service_add(id, "_raop", "_tcp", ctx->port, txt, sizeof(txt) / sizeof(mdns_txt_item_t)) ); - - ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtsp_thread, "RTSP_thread", RTSP_STACK_SIZE, ctx, ESP_TASK_PRIO_MIN + 1, ctx->xStack, ctx->xTaskBuffer); -#endif - - return ctx; -} - -/*----------------------------------------------------------------------------*/ -void raop_delete(struct raop_ctx_s *ctx) { -#ifdef WIN32 - int sock; - struct sockaddr addr; - socklen_t nlen = sizeof(struct sockaddr); -#endif - -if (!ctx) return; - -#ifdef WIN32 - ctx->running = false; - - // wake-up thread by connecting socket, needed for freeBSD - sock = socket(AF_INET, SOCK_STREAM, 0); - getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); - connect(sock, (struct sockaddr*) &addr, sizeof(addr)); - closesocket(sock); - - pthread_join(ctx->thread, NULL); - - rtp_end(ctx->rtp); - - shutdown(ctx->sock, SD_BOTH); - closesocket(ctx->sock); - - // terminate search, but do not reclaim memory of pthread if never launched - if (ctx->active_remote.handle) { - close_mDNS(ctx->active_remote.handle); - pthread_join(ctx->search_thread, NULL); - } - - // stop broadcasting devices - mdns_service_remove(ctx->svr, ctx->svc); - mdnsd_stop(ctx->svr); -#else - // first stop the search task if any - if (ctx->active_remote.running) { - ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); - ctx->active_remote.running = false; - - vTaskResume(ctx->active_remote.thread); - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->active_remote.thread); - - heap_caps_free(ctx->active_remote.xTaskBuffer); - } - - // then the RTSP task - ctx->joiner = xTaskGetCurrentTaskHandle(); - ctx->running = false; - - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->thread); - heap_caps_free(ctx->xTaskBuffer); - - rtp_end(ctx->rtp); - - shutdown(ctx->sock, SHUT_RDWR); - closesocket(ctx->sock); - - mdns_service_remove("_raop", "_tcp"); -#endif - - NFREE(ctx->rtsp.aeskey); - NFREE(ctx->rtsp.aesiv); - NFREE(ctx->rtsp.fmtp); - - free(ctx); -} - -/*----------------------------------------------------------------------------*/ -void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param) { - struct sockaddr_in addr; - int sock; - char *command = NULL; - - // first notify the remote controller (if any) - switch(event) { - case RAOP_REW: - command = strdup("beginrew"); - break; - case RAOP_FWD: - command = strdup("beginff"); - break; - case RAOP_PREV: - command = strdup("previtem"); - break; - case RAOP_NEXT: - command = strdup("nextitem"); - break; - case RAOP_TOGGLE: - command = strdup("playpause"); - break; - case RAOP_PAUSE: - command = strdup("pause"); - break; - case RAOP_PLAY: - command = strdup("play"); - break; - case RAOP_RESUME: - command = strdup("playresume"); - break; - case RAOP_STOP: - command = strdup("stop"); - break; - case RAOP_VOLUME_UP: - command = strdup("volumeup"); - break; - case RAOP_VOLUME_DOWN: - command = strdup("volumedown"); - break; - case RAOP_VOLUME: { - float Volume = *((float*) param); - Volume = Volume ? (Volume - 1) * 30 : -144; - asprintf(&command,"setproperty?dmcp.device-volume=%0.4lf", Volume); - break; - } - default: - break; - } - - // no command to send to remote or no remote found yet - if (!command || !ctx->active_remote.port) { - NFREE(command); - return; - } - - sock = socket(AF_INET, SOCK_STREAM, 0); - - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = S_ADDR(ctx->active_remote.host); - addr.sin_port = htons(ctx->active_remote.port); - - if (!connect(sock, (struct sockaddr*) &addr, sizeof(addr))) { - char *method, *buf, resp[512] = ""; - int len; - key_data_t headers[4] = { {NULL, NULL} }; - - asprintf(&method, "GET /ctrl-int/1/%s HTTP/1.0", command); - kd_add(headers, "Active-Remote", ctx->active_remote.id); - kd_add(headers, "Connection", "close"); - - buf = http_send(sock, method, headers); - len = recv(sock, resp, 512, 0); - if (len > 0) resp[len-1] = '\0'; - LOG_INFO("[%p]: sending airplay remote\n%s<== received ==>\n%s", ctx, buf, resp); - - NFREE(method); - NFREE(buf); - kd_free(headers); - } - - free(command); - closesocket(sock); -} - -/*----------------------------------------------------------------------------*/ -static void *rtsp_thread(void *arg) { - raop_ctx_t *ctx = (raop_ctx_t*) arg; - int sock = -1; - - while (ctx->running) { - fd_set rfds; - struct timeval timeout = {0, 100*1000}; - int n; - bool res = false; - - if (sock == -1) { - struct sockaddr_in peer; - socklen_t addrlen = sizeof(struct sockaddr_in); - - sock = accept(ctx->sock, (struct sockaddr*) &peer, &addrlen); - ctx->peer.s_addr = peer.sin_addr.s_addr; - - if (sock != -1 && ctx->running) { - LOG_INFO("got RTSP connection %u", sock); - } else continue; - } - - FD_ZERO(&rfds); - FD_SET(sock, &rfds); - - n = select(sock + 1, &rfds, NULL, NULL, &timeout); - - if (!n) continue; - - if (n > 0) res = handle_rtsp(ctx, sock); - - if (n < 0 || !res) { - closesocket(sock); - LOG_INFO("RTSP close %u", sock); - sock = -1; - } - } - - if (sock != -1) closesocket(sock); - -#ifndef WIN32 - xTaskNotifyGive(ctx->joiner); - vTaskSuspend(NULL); -#endif - - return NULL; -} - - -/*----------------------------------------------------------------------------*/ -static bool handle_rtsp(raop_ctx_t *ctx, int sock) -{ - char *buf = NULL, *body = NULL, method[16] = ""; - key_data_t headers[16], resp[8] = { {NULL, NULL} }; - int len; - bool success = true; - - if (!http_parse(sock, method, headers, &body, &len)) { - NFREE(body); - kd_free(headers); - return false; - } - - if (strcmp(method, "OPTIONS")) { - LOG_INFO("[%p]: received %s", ctx, method); - } - - if ((buf = kd_lookup(headers, "Apple-Challenge")) != NULL) { - int n; - char *buf_pad, *p, *data_b64 = NULL, data[32]; - - LOG_INFO("[%p]: challenge %s", ctx, buf); - - // need to pad the base64 string as apple device don't - base64_pad(buf, &buf_pad); - - p = data + min(base64_decode(buf_pad, data), 32-10); - p = (char*) memcpy(p, &S_ADDR(ctx->host), 4) + 4; - p = (char*) memcpy(p, ctx->mac, 6) + 6; - memset(p, 0, 32 - (p - data)); - p = rsa_apply((unsigned char*) data, 32, &n, RSA_MODE_AUTH); - n = base64_encode(p, n, &data_b64); - - // remove padding as well (seems to be optional now) - for (n = strlen(data_b64) - 1; n > 0 && data_b64[n] == '='; data_b64[n--] = '\0'); - - kd_add(resp, "Apple-Response", data_b64); - - NFREE(p); - NFREE(buf_pad); - NFREE(data_b64); - } - - if (!strcmp(method, "OPTIONS")) { - - kd_add(resp, "Public", "ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER"); - - } else if (!strcmp(method, "ANNOUNCE")) { - char *padded, *p; - - NFREE(ctx->rtsp.aeskey); - NFREE(ctx->rtsp.aesiv); - NFREE(ctx->rtsp.fmtp); - - // LMS might has taken over the player, leaving us with a running RTP session (should not happen) - if (ctx->rtp) { - LOG_WARN("[%p]: closing unfinished RTP session", ctx); - rtp_end(ctx->rtp); - } - - // same, should not happen unless we have missed a teardown ... - if (ctx->active_remote.running) { - ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); - ctx->active_remote.running = false; - - vTaskResume(ctx->active_remote.thread); - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->active_remote.thread); - - heap_caps_free(ctx->active_remote.xTaskBuffer); - memset(&ctx->active_remote, 0, sizeof(ctx->active_remote)); - - LOG_WARN("[%p]: closing unfinished mDNS search", ctx); - } - - if ((p = strcasestr(body, "rsaaeskey")) != NULL) { - unsigned char *aeskey; - int len, outlen; - - p = strextract(p, ":", "\r\n"); - base64_pad(p, &padded); - aeskey = malloc(strlen(padded)); - len = base64_decode(padded, aeskey); - ctx->rtsp.aeskey = rsa_apply(aeskey, len, &outlen, RSA_MODE_KEY); - - NFREE(p); - NFREE(aeskey); - NFREE(padded); - } - - if ((p = strcasestr(body, "aesiv")) != NULL) { - p = strextract(p, ":", "\r\n"); - base64_pad(p, &padded); - ctx->rtsp.aesiv = malloc(strlen(padded)); - base64_decode(padded, ctx->rtsp.aesiv); - - NFREE(p); - NFREE(padded); - } - - if ((p = strcasestr(body, "fmtp")) != NULL) { - p = strextract(p, ":", "\r\n"); - ctx->rtsp.fmtp = strdup(p); - NFREE(p); - } - - // on announce, search remote - if ((buf = kd_lookup(headers, "DACP-ID")) != NULL) strcpy(ctx->active_remote.DACPid, buf); - if ((buf = kd_lookup(headers, "Active-Remote")) != NULL) strcpy(ctx->active_remote.id, buf); - -#ifdef WIN32 - ctx->active_remote.handle = init_mDNS(false, ctx->host); - pthread_create(&ctx->search_thread, NULL, &search_remote, ctx); -#else - ctx->active_remote.running = true; - ctx->active_remote.xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ctx->active_remote.thread = xTaskCreateStatic( (TaskFunction_t) search_remote, "search_remote", SEARCH_STACK_SIZE, ctx, ESP_TASK_PRIO_MIN + 1, ctx->active_remote.xStack, ctx->active_remote.xTaskBuffer); -#endif - - } else if (!strcmp(method, "SETUP") && ((buf = kd_lookup(headers, "Transport")) != NULL)) { - char *p; - rtp_resp_t rtp = { 0 }; - short unsigned tport = 0, cport = 0; - - // we are about to stream, do something if needed - success = ctx->cmd_cb(RAOP_SETUP, NULL); - - if ((p = strcasestr(buf, "timing_port")) != NULL) sscanf(p, "%*[^=]=%hu", &tport); - if ((p = strcasestr(buf, "control_port")) != NULL) sscanf(p, "%*[^=]=%hu", &cport); - - rtp = rtp_init(ctx->peer, ctx->latency, ctx->rtsp.aeskey, ctx->rtsp.aesiv, - ctx->rtsp.fmtp, cport, tport, ctx->cmd_cb, ctx->data_cb); - - ctx->rtp = rtp.ctx; - - if (cport * tport * rtp.cport * rtp.tport * rtp.aport && rtp.ctx) { - char *transport; - asprintf(&transport, "RTP/AVP/UDP;unicast;mode=record;control_port=%u;timing_port=%u;server_port=%u", rtp.cport, rtp.tport, rtp.aport); - LOG_DEBUG("[%p]: audio=(%hu:%hu), timing=(%hu:%hu), control=(%hu:%hu)", ctx, 0, rtp.aport, tport, rtp.tport, cport, rtp.cport); - kd_add(resp, "Transport", transport); - kd_add(resp, "Session", "DEADBEEF"); - free(transport); - } else { - success = false; - LOG_INFO("[%p]: cannot start session, missing ports", ctx); - } - - } else if (!strcmp(method, "RECORD")) { - unsigned short seqno = 0; - unsigned rtptime = 0; - char *p; - - if (ctx->latency) { - char latency[6]; - snprintf(latency, 6, "%u", ctx->latency); - kd_add(resp, "Audio-Latency", latency); - } - - buf = kd_lookup(headers, "RTP-Info"); - if (buf && (p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); - if (buf && (p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); - - if (ctx->rtp) rtp_record(ctx->rtp, seqno, rtptime); - - success = ctx->cmd_cb(RAOP_STREAM, NULL); - - } else if (!strcmp(method, "FLUSH")) { - unsigned short seqno = 0; - unsigned rtptime = 0; - char *p; - - buf = kd_lookup(headers, "RTP-Info"); - if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); - if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); - - // only send FLUSH if useful (discards frames above buffer head and top) - if (ctx->rtp && rtp_flush(ctx->rtp, seqno, rtptime)) - success = ctx->cmd_cb(RAOP_FLUSH, NULL); - - } else if (!strcmp(method, "TEARDOWN")) { - - rtp_end(ctx->rtp); - - ctx->rtp = NULL; - - // need to make sure no search is on-going and reclaim pthread memory -#ifdef WIN32 - if (ctx->active_remote.handle) close_mDNS(ctx->active_remote.handle); - pthread_join(ctx->search_thread, NULL); -#else - ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); - ctx->active_remote.running = false; - - // task might not need to be resumed anyway - vTaskResume(ctx->active_remote.thread); - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->active_remote.thread); - - heap_caps_free(ctx->active_remote.xTaskBuffer); - - LOG_INFO("[%p]: mDNS search task terminated", ctx); -#endif - - memset(&ctx->active_remote, 0, sizeof(ctx->active_remote)); - NFREE(ctx->rtsp.aeskey); - NFREE(ctx->rtsp.aesiv); - NFREE(ctx->rtsp.fmtp); - - success = ctx->cmd_cb(RAOP_STOP, NULL); - - } else if (!strcmp(method, "SET_PARAMETER")) { - char *p; - - if (body && (p = strcasestr(body, "volume")) != NULL) { - float volume; - - sscanf(p, "%*[^:]:%f", &volume); - LOG_INFO("[%p]: SET PARAMETER volume %f", ctx, volume); - volume = (volume == -144.0) ? 0 : (1 + volume / 30); - success = ctx->cmd_cb(RAOP_VOLUME, &volume); - } -/* - if (body && ((p = kd_lookup(headers, "Content-Type")) != NULL) && !strcasecmp(p, "application/x-dmap-tagged")) { - struct metadata_s metadata; - dmap_settings settings = { - NULL, NULL, NULL, NULL, NULL, NULL, NULL, on_dmap_string, NULL, - NULL - }; - - settings.ctx = &metadata; - memset(&metadata, 0, sizeof(struct metadata_s)); - if (!dmap_parse(&settings, body, len)) { - LOG_INFO("[%p]: received metadata\n\tartist: %s\n\talbum: %s\n\ttitle: %s", - ctx, metadata.artist, metadata.album, metadata.title); - free_metadata(&metadata); - } - } -*/ - } - - // don't need to free "buf" because kd_lookup return a pointer, not a strdup - kd_add(resp, "Audio-Jack-Status", "connected; type=analog"); - kd_add(resp, "CSeq", kd_lookup(headers, "CSeq")); - - if (success) buf = http_send(sock, "RTSP/1.0 200 OK", resp); - else buf = http_send(sock, "RTSP/1.0 500 ERROR", NULL); - - if (strcmp(method, "OPTIONS")) { - LOG_INFO("[%p]: responding:\n%s", ctx, buf ? buf : ""); - } - - NFREE(body); - NFREE(buf); - kd_free(resp); - kd_free(headers); - - return true; -} - -/*----------------------------------------------------------------------------*/ -#ifdef WIN32 -bool search_remote_cb(mDNSservice_t *slist, void *cookie, bool *stop) { - mDNSservice_t *s; - raop_ctx_t *ctx = (raop_ctx_t*) cookie; - - // see if we have found an active remote for our ID - for (s = slist; s; s = s->next) { - if (strcasestr(s->name, ctx->active_remote.DACPid)) { - ctx->active_remote.host = s->addr; - ctx->active_remote.port = s->port; - LOG_INFO("[%p]: found ActiveRemote for %s at %s:%u", ctx, ctx->active_remote.DACPid, - inet_ntoa(ctx->active_remote.host), ctx->active_remote.port); - *stop = true; - break; - } - } - - // let caller clear list - return false; -} - - -/*----------------------------------------------------------------------------*/ -static void* search_remote(void *args) { - raop_ctx_t *ctx = (raop_ctx_t*) args; - - query_mDNS(ctx->active_remote.handle, "_dacp._tcp.local", 0, 0, &search_remote_cb, (void*) ctx); - - return NULL; -} - -#else - -/*----------------------------------------------------------------------------*/ -static void* search_remote(void *args) { - raop_ctx_t *ctx = (raop_ctx_t*) args; - bool found = false; - - LOG_INFO("starting remote search"); - - while (ctx->active_remote.running && !found) { - mdns_result_t *results = NULL; - mdns_result_t *r; - mdns_ip_addr_t *a; - - if (mdns_query_ptr("_dacp", "_tcp", 3000, 32, &results)) { - LOG_ERROR("mDNS active remote query Failed"); - continue; - } - - for (r = results; r && !strcasestr(r->instance_name, ctx->active_remote.DACPid); r = r->next); - if (r) { - for (a = r->addr; a && a->addr.type != IPADDR_TYPE_V4; a = a->next); - if (a) { - found = true; - ctx->active_remote.host.s_addr = a->addr.u_addr.ip4.addr; - ctx->active_remote.port = r->port; - LOG_INFO("found remote %s %s:%hu", r->instance_name, inet_ntoa(ctx->active_remote.host), ctx->active_remote.port); - } - } - - mdns_query_results_free(results); - } - - /* - for some reason which is beyond me, if that tasks gives the semaphore - before the RTSP tasks waits for it, then freeRTOS crashes in queue - management caused by LWIP stack once the RTSP socket is closed. I have - no clue why, but so we'll suspend the tasks as soon as we're done with - search and wait for the resume then give the semaphore - */ - // PS: I know this is not fully race-condition free - if (ctx->active_remote.running) vTaskSuspend(NULL); - xTaskNotifyGive(ctx->active_remote.joiner); - - // now our context will be deleted - vTaskSuspend(NULL); - - return NULL; - } -#endif - - /*----------------------------------------------------------------------------*/ -static char *rsa_apply(unsigned char *input, int inlen, int *outlen, int mode) -{ - static char super_secret_key[] = - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" - "wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" - "wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" - "/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" - "UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" - "BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" - "LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" - "NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" - "lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" - "aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" - "a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" - "oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" - "oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" - "k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" - "AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" - "cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" - "54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" - "17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" - "1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" - "LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" - "2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n" - "-----END RSA PRIVATE KEY-----"; -#ifdef WIN32 - unsigned char *out; - RSA *rsa; - - BIO *bmem = BIO_new_mem_buf(super_secret_key, -1); - rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL); - BIO_free(bmem); - - out = malloc(RSA_size(rsa)); - switch (mode) { - case RSA_MODE_AUTH: - *outlen = RSA_private_encrypt(inlen, input, out, rsa, - RSA_PKCS1_PADDING); - break; - case RSA_MODE_KEY: - *outlen = RSA_private_decrypt(inlen, input, out, rsa, - RSA_PKCS1_OAEP_PADDING); - break; - } - - RSA_free(rsa); - - return (char*) out; -#else - mbedtls_pk_context pkctx; - mbedtls_rsa_context *trsa; - size_t olen; - - /* - we should do entropy initialization & pass a rng function but this - consumes a ton of stack and there is no security concern here. Anyway, - mbedtls takes a lot of stack, unfortunately ... - */ - - mbedtls_pk_init(&pkctx); - mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, - sizeof(super_secret_key), NULL, 0); - - uint8_t *outbuf = NULL; - trsa = mbedtls_pk_rsa(pkctx); - - switch (mode) { - case RSA_MODE_AUTH: - mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE); - outbuf = malloc(trsa->len); - mbedtls_rsa_pkcs1_encrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, inlen, input, outbuf); - *outlen = trsa->len; - break; - case RSA_MODE_KEY: - mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA1); - outbuf = malloc(trsa->len); - mbedtls_rsa_pkcs1_decrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, &olen, input, outbuf, trsa->len); - *outlen = olen; - break; - } - - mbedtls_pk_free(&pkctx); - - return (char*) outbuf; -#endif -} - -#define DECODE_ERROR 0xffffffff - -static char base64_chars[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -/*----------------------------------------------------------------------------*/ -static int base64_pad(char *src, char **padded) -{ - int n; - - n = strlen(src) + strlen(src) % 4; - *padded = malloc(n + 1); - memset(*padded, '=', n); - memcpy(*padded, src, strlen(src)); - (*padded)[n] = '\0'; - - return strlen(*padded); -} - -/*----------------------------------------------------------------------------*/ -static int pos(char c) -{ - char *p; - for (p = base64_chars; *p; p++) - if (*p == c) - return p - base64_chars; - return -1; -} - -/*----------------------------------------------------------------------------*/ -static int base64_encode(const void *data, int size, char **str) -{ - char *s, *p; - int i; - int c; - const unsigned char *q; - - p = s = (char *) malloc(size * 4 / 3 + 4); - if (p == NULL) return -1; - q = (const unsigned char *) data; - i = 0; - for (i = 0; i < size;) { - c = q[i++]; - c *= 256; - if (i < size) c += q[i]; - i++; - c *= 256; - if (i < size) c += q[i]; - i++; - p[0] = base64_chars[(c & 0x00fc0000) >> 18]; - p[1] = base64_chars[(c & 0x0003f000) >> 12]; - p[2] = base64_chars[(c & 0x00000fc0) >> 6]; - p[3] = base64_chars[(c & 0x0000003f) >> 0]; - if (i > size) p[3] = '='; - if (i > size + 1) p[2] = '='; - p += 4; - } - *p = 0; - *str = s; - return strlen(s); -} - -/*----------------------------------------------------------------------------*/ -static unsigned int token_decode(const char *token) -{ - int i; - unsigned int val = 0; - int marker = 0; - if (strlen(token) < 4) - return DECODE_ERROR; - for (i = 0; i < 4; i++) { - val *= 64; - if (token[i] == '=') - marker++; - else if (marker > 0) - return DECODE_ERROR; - else - val += pos(token[i]); - } - if (marker > 2) - return DECODE_ERROR; - return (marker << 24) | val; -} - -/*----------------------------------------------------------------------------*/ -static int base64_decode(const char *str, void *data) -{ - const char *p; - unsigned char *q; - - q = data; - for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) { - unsigned int val = token_decode(p); - unsigned int marker = (val >> 24) & 0xff; - if (val == DECODE_ERROR) - return -1; - *q++ = (val >> 16) & 0xff; - if (marker < 2) - *q++ = (val >> 8) & 0xff; - if (marker < 1) - *q++ = val & 0xff; - } - return q - (unsigned char *) data; -} - -/*----------------------------------------------------------------------------*/ -static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { - struct metadata_s *metadata = (struct metadata_s *) ctx; - - if (!strcasecmp(code, "asar")) metadata->artist = strndup(buf, len); - else if (!strcasecmp(code, "asal")) metadata->album = strndup(buf, len); - else if (!strcasecmp(code, "minm")) metadata->title = strndup(buf, len); -} - +/* + * + * (c) Philippe 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include "platform.h" + +#ifdef WIN32 +#include +#include +#include +#include +#include +#include "mdns.h" +#include "mdnsd.h" +#include "mdnssd-itf.h" +#else +#include "esp_pthread.h" +#include "mdns.h" +#include "mbedtls/version.h" +#include +#endif + +#include "util.h" +#include "raop.h" +#include "rtp.h" +#include "dmap_parser.h" +#include "log_util.h" + +#define RTSP_STACK_SIZE (8*1024) +#define SEARCH_STACK_SIZE (2*1048) + +typedef struct raop_ctx_s { +#ifdef WIN32 + struct mdns_service *svc; + struct mdnsd *svr; +#endif + struct in_addr host; // IP of bridge + short unsigned port; // RTSP port for AirPlay + int sock; // socket of the above + struct in_addr peer; // IP of the iDevice (airplay sender) + bool running; +#ifdef WIN32 + pthread_t thread, search_thread; +#else + TaskHandle_t thread, search_thread, joiner; + StaticTask_t *xTaskBuffer; + StackType_t xStack[RTSP_STACK_SIZE] __attribute__ ((aligned (4))); +#endif + unsigned char mac[6]; + int latency; + struct { + char *aesiv, *aeskey; + char *fmtp; + } rtsp; + struct rtp_s *rtp; + raop_cmd_cb_t cmd_cb; + raop_data_cb_t data_cb; + struct { + char DACPid[32], id[32]; + struct in_addr host; + u16_t port; +#ifdef WIN32 + struct mDNShandle_s *handle; +#else + bool running; + TaskHandle_t thread, joiner; + StaticTask_t *xTaskBuffer; + StackType_t xStack[SEARCH_STACK_SIZE] __attribute__ ((aligned (4)));; +#endif + } active_remote; + void *owner; +} raop_ctx_t; + +extern struct mdnsd* glmDNSServer; +extern log_level raop_loglevel; +static log_level *loglevel = &raop_loglevel; + +static void* rtsp_thread(void *arg); +static bool handle_rtsp(raop_ctx_t *ctx, int sock); + +static char* rsa_apply(unsigned char *input, int inlen, int *outlen, int mode); +static int base64_pad(char *src, char **padded); +static int base64_encode(const void *data, int size, char **str); +static int base64_decode(const char *str, void *data); +static void* search_remote(void *args); + +extern char private_key[]; + enum { RSA_MODE_KEY, RSA_MODE_AUTH }; + +static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len); + +/*----------------------------------------------------------------------------*/ +struct raop_ctx_s *raop_create(struct in_addr host, char *name, + unsigned char mac[6], int latency, + raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) { + struct raop_ctx_s *ctx = malloc(sizeof(struct raop_ctx_s)); + struct sockaddr_in addr; + char id[64]; + #ifdef WIN32 + socklen_t nlen = sizeof(struct sockaddr); + char *txt[] = { "am=airesp32", "tp=UDP", "sm=false", "sv=false", "ek=1", + "et=0,1", "md=0,1,2", "cn=0,1", "ch=2", + "ss=16", "sr=44100", "vn=3", "txtvers=1", + NULL }; +#else + mdns_txt_item_t txt[] = { + {"am", "airesp32"}, + {"tp", "UDP"}, + {"sm","false"}, + {"sv","false"}, + {"ek","1"}, + {"et","0,1"}, + {"md","0,1,2"}, + {"cn","0,1"}, + {"ch","2"}, + {"ss","16"}, + {"sr","44100"}, + {"vn","3"}, + {"txtvers","1"}, + }; + +#endif + + if (!ctx) return NULL; + + // make sure we have a clean context + memset(ctx, 0, sizeof(raop_ctx_t)); + +#ifdef WIN32 + ctx->svr = glmDNSServer; +#endif + ctx->host = host; + ctx->sock = socket(AF_INET, SOCK_STREAM, 0); + ctx->cmd_cb = cmd_cb; + ctx->data_cb = data_cb; + ctx->latency = min(latency, 88200); + if (ctx->sock == -1) { + LOG_ERROR("Cannot create listening socket", NULL); + free(ctx); + return NULL; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_addr.s_addr = host.s_addr; + addr.sin_family = AF_INET; +#ifdef WIN32 + ctx->port = 0; + addr.sin_port = htons(ctx->port); +#else + ctx->port = 5000; + addr.sin_port = htons(ctx->port); +#endif + + if (bind(ctx->sock, (struct sockaddr *) &addr, sizeof(addr)) < 0 || listen(ctx->sock, 1)) { + LOG_ERROR("Cannot bind or listen RTSP listener: %s", strerror(errno)); + free(ctx); + closesocket(ctx->sock); + return NULL; + } + +#ifdef WIN32 + getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); + ctx->port = ntohs(addr.sin_port); +#endif + ctx->running = true; + memcpy(ctx->mac, mac, 6); + snprintf(id, 64, "%02X%02X%02X%02X%02X%02X@%s", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], name); + #ifdef WIN32 + // seems that Windows snprintf does not add NULL char if actual size > max + id[63] = '\0'; + ctx->svc = mdnsd_register_svc(ctx->svr, id, "_raop._tcp.local", ctx->port, NULL, (const char**) txt); + pthread_create(&ctx->thread, NULL, &rtsp_thread, ctx); + #else + LOG_INFO("starting mDNS with %s", id); + ESP_ERROR_CHECK( mdns_service_add(id, "_raop", "_tcp", ctx->port, txt, sizeof(txt) / sizeof(mdns_txt_item_t)) ); + + ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtsp_thread, "RTSP_thread", RTSP_STACK_SIZE, ctx, ESP_TASK_PRIO_MIN + 1, ctx->xStack, ctx->xTaskBuffer); +#endif + + return ctx; +} + +/*----------------------------------------------------------------------------*/ +void raop_delete(struct raop_ctx_s *ctx) { +#ifdef WIN32 + int sock; + struct sockaddr addr; + socklen_t nlen = sizeof(struct sockaddr); +#endif + +if (!ctx) return; + +#ifdef WIN32 + ctx->running = false; + + // wake-up thread by connecting socket, needed for freeBSD + sock = socket(AF_INET, SOCK_STREAM, 0); + getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen); + connect(sock, (struct sockaddr*) &addr, sizeof(addr)); + closesocket(sock); + + pthread_join(ctx->thread, NULL); + + rtp_end(ctx->rtp); + + shutdown(ctx->sock, SD_BOTH); + closesocket(ctx->sock); + + // terminate search, but do not reclaim memory of pthread if never launched + if (ctx->active_remote.handle) { + close_mDNS(ctx->active_remote.handle); + pthread_join(ctx->search_thread, NULL); + } + + // stop broadcasting devices + mdns_service_remove(ctx->svr, ctx->svc); + mdnsd_stop(ctx->svr); +#else + // first stop the search task if any + if (ctx->active_remote.running) { + ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); + ctx->active_remote.running = false; + + vTaskResume(ctx->active_remote.thread); + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->active_remote.thread); + + heap_caps_free(ctx->active_remote.xTaskBuffer); + } + + // then the RTSP task + ctx->joiner = xTaskGetCurrentTaskHandle(); + ctx->running = false; + + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->thread); + heap_caps_free(ctx->xTaskBuffer); + + rtp_end(ctx->rtp); + + shutdown(ctx->sock, SHUT_RDWR); + closesocket(ctx->sock); + + mdns_service_remove("_raop", "_tcp"); +#endif + + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + free(ctx); +} + +/*----------------------------------------------------------------------------*/ +void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param) { + struct sockaddr_in addr; + int sock; + char *command = NULL; + + // first notify the remote controller (if any) + switch(event) { + case RAOP_REW: + command = strdup("beginrew"); + break; + case RAOP_FWD: + command = strdup("beginff"); + break; + case RAOP_PREV: + command = strdup("previtem"); + break; + case RAOP_NEXT: + command = strdup("nextitem"); + break; + case RAOP_TOGGLE: + command = strdup("playpause"); + break; + case RAOP_PAUSE: + command = strdup("pause"); + break; + case RAOP_PLAY: + command = strdup("play"); + break; + case RAOP_RESUME: + command = strdup("playresume"); + break; + case RAOP_STOP: + command = strdup("stop"); + break; + case RAOP_VOLUME_UP: + command = strdup("volumeup"); + break; + case RAOP_VOLUME_DOWN: + command = strdup("volumedown"); + break; + case RAOP_VOLUME: { + float Volume = *((float*) param); + Volume = Volume ? (Volume - 1) * 30 : -144; + asprintf(&command,"setproperty?dmcp.device-volume=%0.4lf", Volume); + break; + } + default: + break; + } + + // no command to send to remote or no remote found yet + if (!command || !ctx->active_remote.port) { + NFREE(command); + return; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = S_ADDR(ctx->active_remote.host); + addr.sin_port = htons(ctx->active_remote.port); + + if (!connect(sock, (struct sockaddr*) &addr, sizeof(addr))) { + char *method, *buf, resp[512] = ""; + int len; + key_data_t headers[4] = { {NULL, NULL} }; + + asprintf(&method, "GET /ctrl-int/1/%s HTTP/1.0", command); + kd_add(headers, "Active-Remote", ctx->active_remote.id); + kd_add(headers, "Connection", "close"); + + buf = http_send(sock, method, headers); + len = recv(sock, resp, 512, 0); + if (len > 0) resp[len-1] = '\0'; + LOG_INFO("[%p]: sending airplay remote\n%s<== received ==>\n%s", ctx, buf, resp); + + NFREE(method); + NFREE(buf); + kd_free(headers); + } + + free(command); + closesocket(sock); +} + +/*----------------------------------------------------------------------------*/ +static void *rtsp_thread(void *arg) { + raop_ctx_t *ctx = (raop_ctx_t*) arg; + int sock = -1; + + while (ctx->running) { + fd_set rfds; + struct timeval timeout = {0, 100*1000}; + int n; + bool res = false; + + if (sock == -1) { + struct sockaddr_in peer; + socklen_t addrlen = sizeof(struct sockaddr_in); + + sock = accept(ctx->sock, (struct sockaddr*) &peer, &addrlen); + ctx->peer.s_addr = peer.sin_addr.s_addr; + + if (sock != -1 && ctx->running) { + LOG_INFO("got RTSP connection %u", sock); + } else continue; + } + + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + + n = select(sock + 1, &rfds, NULL, NULL, &timeout); + + if (!n) continue; + + if (n > 0) res = handle_rtsp(ctx, sock); + + if (n < 0 || !res) { + closesocket(sock); + LOG_INFO("RTSP close %u", sock); + sock = -1; + } + } + + if (sock != -1) closesocket(sock); + +#ifndef WIN32 + xTaskNotifyGive(ctx->joiner); + vTaskSuspend(NULL); +#endif + + return NULL; +} + + +/*----------------------------------------------------------------------------*/ +static bool handle_rtsp(raop_ctx_t *ctx, int sock) +{ + char *buf = NULL, *body = NULL, method[16] = ""; + key_data_t headers[16], resp[8] = { {NULL, NULL} }; + int len; + bool success = true; + + if (!http_parse(sock, method, headers, &body, &len)) { + NFREE(body); + kd_free(headers); + return false; + } + + if (strcmp(method, "OPTIONS")) { + LOG_INFO("[%p]: received %s", ctx, method); + } + + if ((buf = kd_lookup(headers, "Apple-Challenge")) != NULL) { + int n; + char *buf_pad, *p, *data_b64 = NULL, data[32]; + + LOG_INFO("[%p]: challenge %s", ctx, buf); + + // need to pad the base64 string as apple device don't + base64_pad(buf, &buf_pad); + + p = data + min(base64_decode(buf_pad, data), 32-10); + p = (char*) memcpy(p, &S_ADDR(ctx->host), 4) + 4; + p = (char*) memcpy(p, ctx->mac, 6) + 6; + memset(p, 0, 32 - (p - data)); + p = rsa_apply((unsigned char*) data, 32, &n, RSA_MODE_AUTH); + n = base64_encode(p, n, &data_b64); + + // remove padding as well (seems to be optional now) + for (n = strlen(data_b64) - 1; n > 0 && data_b64[n] == '='; data_b64[n--] = '\0'); + + kd_add(resp, "Apple-Response", data_b64); + + NFREE(p); + NFREE(buf_pad); + NFREE(data_b64); + } + + if (!strcmp(method, "OPTIONS")) { + + kd_add(resp, "Public", "ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER"); + + } else if (!strcmp(method, "ANNOUNCE")) { + char *padded, *p; + + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + // LMS might has taken over the player, leaving us with a running RTP session (should not happen) + if (ctx->rtp) { + LOG_WARN("[%p]: closing unfinished RTP session", ctx); + rtp_end(ctx->rtp); + } + + // same, should not happen unless we have missed a teardown ... + if (ctx->active_remote.running) { + ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); + ctx->active_remote.running = false; + + vTaskResume(ctx->active_remote.thread); + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->active_remote.thread); + + heap_caps_free(ctx->active_remote.xTaskBuffer); + memset(&ctx->active_remote, 0, sizeof(ctx->active_remote)); + + LOG_WARN("[%p]: closing unfinished mDNS search", ctx); + } + + if ((p = strcasestr(body, "rsaaeskey")) != NULL) { + unsigned char *aeskey; + int len, outlen; + + p = strextract(p, ":", "\r\n"); + base64_pad(p, &padded); + aeskey = malloc(strlen(padded)); + len = base64_decode(padded, aeskey); + ctx->rtsp.aeskey = rsa_apply(aeskey, len, &outlen, RSA_MODE_KEY); + + NFREE(p); + NFREE(aeskey); + NFREE(padded); + } + + if ((p = strcasestr(body, "aesiv")) != NULL) { + p = strextract(p, ":", "\r\n"); + base64_pad(p, &padded); + ctx->rtsp.aesiv = malloc(strlen(padded)); + base64_decode(padded, ctx->rtsp.aesiv); + + NFREE(p); + NFREE(padded); + } + + if ((p = strcasestr(body, "fmtp")) != NULL) { + p = strextract(p, ":", "\r\n"); + ctx->rtsp.fmtp = strdup(p); + NFREE(p); + } + + // on announce, search remote + if ((buf = kd_lookup(headers, "DACP-ID")) != NULL) strcpy(ctx->active_remote.DACPid, buf); + if ((buf = kd_lookup(headers, "Active-Remote")) != NULL) strcpy(ctx->active_remote.id, buf); + +#ifdef WIN32 + ctx->active_remote.handle = init_mDNS(false, ctx->host); + pthread_create(&ctx->search_thread, NULL, &search_remote, ctx); +#else + ctx->active_remote.running = true; + ctx->active_remote.xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ctx->active_remote.thread = xTaskCreateStatic( (TaskFunction_t) search_remote, "search_remote", SEARCH_STACK_SIZE, ctx, ESP_TASK_PRIO_MIN + 1, ctx->active_remote.xStack, ctx->active_remote.xTaskBuffer); +#endif + + } else if (!strcmp(method, "SETUP") && ((buf = kd_lookup(headers, "Transport")) != NULL)) { + char *p; + rtp_resp_t rtp = { 0 }; + short unsigned tport = 0, cport = 0; + + // we are about to stream, do something if needed + success = ctx->cmd_cb(RAOP_SETUP, NULL); + + if ((p = strcasestr(buf, "timing_port")) != NULL) sscanf(p, "%*[^=]=%hu", &tport); + if ((p = strcasestr(buf, "control_port")) != NULL) sscanf(p, "%*[^=]=%hu", &cport); + + rtp = rtp_init(ctx->peer, ctx->latency, ctx->rtsp.aeskey, ctx->rtsp.aesiv, + ctx->rtsp.fmtp, cport, tport, ctx->cmd_cb, ctx->data_cb); + + ctx->rtp = rtp.ctx; + + if (cport * tport * rtp.cport * rtp.tport * rtp.aport && rtp.ctx) { + char *transport; + asprintf(&transport, "RTP/AVP/UDP;unicast;mode=record;control_port=%u;timing_port=%u;server_port=%u", rtp.cport, rtp.tport, rtp.aport); + LOG_DEBUG("[%p]: audio=(%hu:%hu), timing=(%hu:%hu), control=(%hu:%hu)", ctx, 0, rtp.aport, tport, rtp.tport, cport, rtp.cport); + kd_add(resp, "Transport", transport); + kd_add(resp, "Session", "DEADBEEF"); + free(transport); + } else { + success = false; + LOG_INFO("[%p]: cannot start session, missing ports", ctx); + } + + } else if (!strcmp(method, "RECORD")) { + unsigned short seqno = 0; + unsigned rtptime = 0; + char *p; + + if (ctx->latency) { + char latency[6]; + snprintf(latency, 6, "%u", ctx->latency); + kd_add(resp, "Audio-Latency", latency); + } + + buf = kd_lookup(headers, "RTP-Info"); + if (buf && (p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); + if (buf && (p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); + + if (ctx->rtp) rtp_record(ctx->rtp, seqno, rtptime); + + success = ctx->cmd_cb(RAOP_STREAM, NULL); + + } else if (!strcmp(method, "FLUSH")) { + unsigned short seqno = 0; + unsigned rtptime = 0; + char *p; + + buf = kd_lookup(headers, "RTP-Info"); + if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno); + if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime); + + // only send FLUSH if useful (discards frames above buffer head and top) + if (ctx->rtp && rtp_flush(ctx->rtp, seqno, rtptime)) + success = ctx->cmd_cb(RAOP_FLUSH, NULL); + + } else if (!strcmp(method, "TEARDOWN")) { + + rtp_end(ctx->rtp); + + ctx->rtp = NULL; + + // need to make sure no search is on-going and reclaim pthread memory +#ifdef WIN32 + if (ctx->active_remote.handle) close_mDNS(ctx->active_remote.handle); + pthread_join(ctx->search_thread, NULL); +#else + ctx->active_remote.joiner = xTaskGetCurrentTaskHandle(); + ctx->active_remote.running = false; + + // task might not need to be resumed anyway + vTaskResume(ctx->active_remote.thread); + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->active_remote.thread); + + heap_caps_free(ctx->active_remote.xTaskBuffer); + + LOG_INFO("[%p]: mDNS search task terminated", ctx); +#endif + + memset(&ctx->active_remote, 0, sizeof(ctx->active_remote)); + NFREE(ctx->rtsp.aeskey); + NFREE(ctx->rtsp.aesiv); + NFREE(ctx->rtsp.fmtp); + + success = ctx->cmd_cb(RAOP_STOP, NULL); + + } else if (!strcmp(method, "SET_PARAMETER")) { + char *p; + + if (body && (p = strcasestr(body, "volume")) != NULL) { + float volume; + + sscanf(p, "%*[^:]:%f", &volume); + LOG_INFO("[%p]: SET PARAMETER volume %f", ctx, volume); + volume = (volume == -144.0) ? 0 : (1 + volume / 30); + success = ctx->cmd_cb(RAOP_VOLUME, &volume); + } +/* + if (body && ((p = kd_lookup(headers, "Content-Type")) != NULL) && !strcasecmp(p, "application/x-dmap-tagged")) { + struct metadata_s metadata; + dmap_settings settings = { + NULL, NULL, NULL, NULL, NULL, NULL, NULL, on_dmap_string, NULL, + NULL + }; + + settings.ctx = &metadata; + memset(&metadata, 0, sizeof(struct metadata_s)); + if (!dmap_parse(&settings, body, len)) { + LOG_INFO("[%p]: received metadata\n\tartist: %s\n\talbum: %s\n\ttitle: %s", + ctx, metadata.artist, metadata.album, metadata.title); + free_metadata(&metadata); + } + } +*/ + } + + // don't need to free "buf" because kd_lookup return a pointer, not a strdup + kd_add(resp, "Audio-Jack-Status", "connected; type=analog"); + kd_add(resp, "CSeq", kd_lookup(headers, "CSeq")); + + if (success) buf = http_send(sock, "RTSP/1.0 200 OK", resp); + else buf = http_send(sock, "RTSP/1.0 500 ERROR", NULL); + + if (strcmp(method, "OPTIONS")) { + LOG_INFO("[%p]: responding:\n%s", ctx, buf ? buf : ""); + } + + NFREE(body); + NFREE(buf); + kd_free(resp); + kd_free(headers); + + return true; +} + +/*----------------------------------------------------------------------------*/ +#ifdef WIN32 +bool search_remote_cb(mDNSservice_t *slist, void *cookie, bool *stop) { + mDNSservice_t *s; + raop_ctx_t *ctx = (raop_ctx_t*) cookie; + + // see if we have found an active remote for our ID + for (s = slist; s; s = s->next) { + if (strcasestr(s->name, ctx->active_remote.DACPid)) { + ctx->active_remote.host = s->addr; + ctx->active_remote.port = s->port; + LOG_INFO("[%p]: found ActiveRemote for %s at %s:%u", ctx, ctx->active_remote.DACPid, + inet_ntoa(ctx->active_remote.host), ctx->active_remote.port); + *stop = true; + break; + } + } + + // let caller clear list + return false; +} + + +/*----------------------------------------------------------------------------*/ +static void* search_remote(void *args) { + raop_ctx_t *ctx = (raop_ctx_t*) args; + + query_mDNS(ctx->active_remote.handle, "_dacp._tcp.local", 0, 0, &search_remote_cb, (void*) ctx); + + return NULL; +} + +#else + +/*----------------------------------------------------------------------------*/ +static void* search_remote(void *args) { + raop_ctx_t *ctx = (raop_ctx_t*) args; + bool found = false; + + LOG_INFO("starting remote search"); + + while (ctx->active_remote.running && !found) { + mdns_result_t *results = NULL; + mdns_result_t *r; + mdns_ip_addr_t *a; + + if (mdns_query_ptr("_dacp", "_tcp", 3000, 32, &results)) { + LOG_ERROR("mDNS active remote query Failed"); + continue; + } + + for (r = results; r && !strcasestr(r->instance_name, ctx->active_remote.DACPid); r = r->next); + if (r) { + for (a = r->addr; a && a->addr.type != IPADDR_TYPE_V4; a = a->next); + if (a) { + found = true; + ctx->active_remote.host.s_addr = a->addr.u_addr.ip4.addr; + ctx->active_remote.port = r->port; + LOG_INFO("found remote %s %s:%hu", r->instance_name, inet_ntoa(ctx->active_remote.host), ctx->active_remote.port); + } + } + + mdns_query_results_free(results); + } + + /* + for some reason which is beyond me, if that tasks gives the semaphore + before the RTSP tasks waits for it, then freeRTOS crashes in queue + management caused by LWIP stack once the RTSP socket is closed. I have + no clue why, but so we'll suspend the tasks as soon as we're done with + search and wait for the resume then give the semaphore + */ + // PS: I know this is not fully race-condition free + if (ctx->active_remote.running) vTaskSuspend(NULL); + xTaskNotifyGive(ctx->active_remote.joiner); + + // now our context will be deleted + vTaskSuspend(NULL); + + return NULL; + } +#endif + + /*----------------------------------------------------------------------------*/ +static char *rsa_apply(unsigned char *input, int inlen, int *outlen, int mode) +{ + static char super_secret_key[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" + "wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" + "wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" + "/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" + "UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" + "BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" + "LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" + "NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" + "lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" + "aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" + "a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" + "oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" + "oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" + "k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" + "AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" + "cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" + "54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" + "17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" + "1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" + "LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" + "2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n" + "-----END RSA PRIVATE KEY-----"; +#ifdef WIN32 + unsigned char *out; + RSA *rsa; + + BIO *bmem = BIO_new_mem_buf(super_secret_key, -1); + rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL); + BIO_free(bmem); + + out = malloc(RSA_size(rsa)); + switch (mode) { + case RSA_MODE_AUTH: + *outlen = RSA_private_encrypt(inlen, input, out, rsa, + RSA_PKCS1_PADDING); + break; + case RSA_MODE_KEY: + *outlen = RSA_private_decrypt(inlen, input, out, rsa, + RSA_PKCS1_OAEP_PADDING); + break; + } + + RSA_free(rsa); + + return (char*) out; +#else + mbedtls_pk_context pkctx; + mbedtls_rsa_context *trsa; + size_t olen; + + /* + we should do entropy initialization & pass a rng function but this + consumes a ton of stack and there is no security concern here. Anyway, + mbedtls takes a lot of stack, unfortunately ... + */ + + mbedtls_pk_init(&pkctx); + mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key, + sizeof(super_secret_key), NULL, 0); + + uint8_t *outbuf = NULL; + trsa = mbedtls_pk_rsa(pkctx); + + switch (mode) { + case RSA_MODE_AUTH: + mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE); + outbuf = malloc(trsa->len); + mbedtls_rsa_pkcs1_encrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, inlen, input, outbuf); + *outlen = trsa->len; + break; + case RSA_MODE_KEY: + mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA1); + outbuf = malloc(trsa->len); + mbedtls_rsa_pkcs1_decrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, &olen, input, outbuf, trsa->len); + *outlen = olen; + break; + } + + mbedtls_pk_free(&pkctx); + + return (char*) outbuf; +#endif +} + +#define DECODE_ERROR 0xffffffff + +static char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/*----------------------------------------------------------------------------*/ +static int base64_pad(char *src, char **padded) +{ + int n; + + n = strlen(src) + strlen(src) % 4; + *padded = malloc(n + 1); + memset(*padded, '=', n); + memcpy(*padded, src, strlen(src)); + (*padded)[n] = '\0'; + + return strlen(*padded); +} + +/*----------------------------------------------------------------------------*/ +static int pos(char c) +{ + char *p; + for (p = base64_chars; *p; p++) + if (*p == c) + return p - base64_chars; + return -1; +} + +/*----------------------------------------------------------------------------*/ +static int base64_encode(const void *data, int size, char **str) +{ + char *s, *p; + int i; + int c; + const unsigned char *q; + + p = s = (char *) malloc(size * 4 / 3 + 4); + if (p == NULL) return -1; + q = (const unsigned char *) data; + i = 0; + for (i = 0; i < size;) { + c = q[i++]; + c *= 256; + if (i < size) c += q[i]; + i++; + c *= 256; + if (i < size) c += q[i]; + i++; + p[0] = base64_chars[(c & 0x00fc0000) >> 18]; + p[1] = base64_chars[(c & 0x0003f000) >> 12]; + p[2] = base64_chars[(c & 0x00000fc0) >> 6]; + p[3] = base64_chars[(c & 0x0000003f) >> 0]; + if (i > size) p[3] = '='; + if (i > size + 1) p[2] = '='; + p += 4; + } + *p = 0; + *str = s; + return strlen(s); +} + +/*----------------------------------------------------------------------------*/ +static unsigned int token_decode(const char *token) +{ + int i; + unsigned int val = 0; + int marker = 0; + if (strlen(token) < 4) + return DECODE_ERROR; + for (i = 0; i < 4; i++) { + val *= 64; + if (token[i] == '=') + marker++; + else if (marker > 0) + return DECODE_ERROR; + else + val += pos(token[i]); + } + if (marker > 2) + return DECODE_ERROR; + return (marker << 24) | val; +} + +/*----------------------------------------------------------------------------*/ +static int base64_decode(const char *str, void *data) +{ + const char *p; + unsigned char *q; + + q = data; + for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) { + unsigned int val = token_decode(p); + unsigned int marker = (val >> 24) & 0xff; + if (val == DECODE_ERROR) + return -1; + *q++ = (val >> 16) & 0xff; + if (marker < 2) + *q++ = (val >> 8) & 0xff; + if (marker < 1) + *q++ = val & 0xff; + } + return q - (unsigned char *) data; +} + +/*----------------------------------------------------------------------------*/ +static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { + struct metadata_s *metadata = (struct metadata_s *) ctx; + + if (!strcasecmp(code, "asar")) metadata->artist = strndup(buf, len); + else if (!strcasecmp(code, "asal")) metadata->album = strndup(buf, len); + else if (!strcasecmp(code, "minm")) metadata->title = strndup(buf, len); +} + diff --git a/components/raop/rtp.c b/components/raop/rtp.c index 5dae377a..7cc57e1a 100644 --- a/components/raop/rtp.c +++ b/components/raop/rtp.c @@ -1,783 +1,783 @@ - -/* - * HairTunes - RAOP packet handler and slave-clocked replay engine - * Copyright (c) James Laird 2011 - * All rights reserved. - * - * Modularisation: philippe_44@outlook.com, 2019 - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "platform.h" -#include "rtp.h" -#include "raop_sink.h" -#include "log_util.h" -#include "util.h" - -#ifdef WIN32 -#include -#include "alac_wrapper.h" -#else -#include "esp_pthread.h" -#include "esp_system.h" -#include -#include -#include "alac_wrapper.h" -#endif - -#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22) -#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10) -#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16) -#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16) -#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000) -#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate)) - - extern log_level raop_loglevel; - static log_level *loglevel = &raop_loglevel; - -//#define __RTP_STORE - -// default buffer size -#define BUFFER_FRAMES ( (150 * RAOP_SAMPLE_RATE * 2) / (352 * 100) ) -#define MAX_PACKET 1408 -#define MIN_LATENCY 11025 -#define MAX_LATENCY ( (120 * RAOP_SAMPLE_RATE * 2) / 100 ) - -#define RTP_STACK_SIZE (4*1024) - -#define RTP_SYNC (0x01) -#define NTP_SYNC (0x02) - -#define RESEND_TO 200 - -enum { DATA = 0, CONTROL, TIMING }; - -static const u8_t silence_frame[MAX_PACKET] = { 0 }; - -typedef u16_t seq_t; -typedef struct audio_buffer_entry { // decoded audio packets - int ready; - u32_t rtptime, last_resend; - s16_t *data; - int len; -} abuf_t; - -typedef struct rtp_s { -#ifdef __RTP_STORE - FILE *rtpIN, *rtpOUT; -#endif - bool running; - unsigned char aesiv[16]; -#ifdef WIN32 - AES_KEY aes; -#else - mbedtls_aes_context aes; -#endif - bool decrypt; - u8_t *decrypt_buf; - u32_t frame_size, frame_duration; - u32_t in_frames, out_frames; - struct in_addr host; - struct sockaddr_in rtp_host; - struct { - unsigned short rport, lport; - int sock; - } rtp_sockets[3]; // data, control, timing - struct timing_s { - u64_t local, remote; - } timing; - struct { - u32_t rtp, time; - u8_t status; - } synchro; - struct { - u32_t time; - seq_t seqno; - u32_t rtptime; - } record; - int latency; // rtp hold depth in samples - u32_t resent_req, resent_rec; // total resent + recovered frames - u32_t silent_frames; // total silence frames - u32_t discarded; - abuf_t audio_buffer[BUFFER_FRAMES]; - seq_t ab_read, ab_write; - pthread_mutex_t ab_mutex; -#ifdef WIN32 - pthread_t thread; -#else - TaskHandle_t thread, joiner; - StaticTask_t *xTaskBuffer; - StackType_t xStack[RTP_STACK_SIZE] __attribute__ ((aligned (4))); -#endif - - struct alac_codec_s *alac_codec; - int flush_seqno; - bool playing; - raop_data_cb_t data_cb; - raop_cmd_cb_t cmd_cb; -} rtp_t; - - -#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) -static void buffer_alloc(abuf_t *audio_buffer, int size); -static void buffer_release(abuf_t *audio_buffer); -static void buffer_reset(abuf_t *audio_buffer); -static void buffer_push_packet(rtp_t *ctx); -static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last); -static bool rtp_request_timing(rtp_t *ctx); -static void* rtp_thread_func(void *arg); -static int seq_order(seq_t a, seq_t b); - -/*---------------------------------------------------------------------------*/ -static struct alac_codec_s* alac_init(int fmtp[32]) { - struct alac_codec_s *alac; - unsigned sample_rate; - unsigned char sample_size, channels; - struct { - uint32_t frameLength; - uint8_t compatibleVersion; - uint8_t bitDepth; - uint8_t pb; - uint8_t mb; - uint8_t kb; - uint8_t numChannels; - uint16_t maxRun; - uint32_t maxFrameBytes; - uint32_t avgBitRate; - uint32_t sampleRate; - } config; - - config.frameLength = htonl(fmtp[1]); - config.compatibleVersion = fmtp[2]; - config.bitDepth = fmtp[3]; - config.pb = fmtp[4]; - config.mb = fmtp[5]; - config.kb = fmtp[6]; - config.numChannels = fmtp[7]; - config.maxRun = htons(fmtp[8]); - config.maxFrameBytes = htonl(fmtp[9]); - config.avgBitRate = htonl(fmtp[10]); - config.sampleRate = htonl(fmtp[11]); - - alac = alac_create_decoder(sizeof(config), (unsigned char*) &config, &sample_size, &sample_rate, &channels); - if (!alac) { - LOG_ERROR("cannot create alac codec", NULL); - return NULL; - } - - return alac; -} - -/*---------------------------------------------------------------------------*/ -rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, char *fmtpstr, - short unsigned pCtrlPort, short unsigned pTimingPort, - raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) -{ - int i = 0; - char *arg; - int fmtp[12]; - bool rc = true; - rtp_t *ctx = calloc(1, sizeof(rtp_t)); - rtp_resp_t resp = { 0, 0, 0, NULL }; - - if (!ctx) return resp; - - ctx->host = host; - ctx->decrypt = false; - ctx->cmd_cb = cmd_cb; - ctx->data_cb = data_cb; - ctx->rtp_host.sin_family = AF_INET; - ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; - pthread_mutex_init(&ctx->ab_mutex, 0); - ctx->flush_seqno = -1; - ctx->latency = latency; - ctx->ab_read = ctx->ab_write; - -#ifdef __RTP_STORE - ctx->rtpIN = fopen("airplay.rtpin", "wb"); - ctx->rtpOUT = fopen("airplay.rtpout", "wb"); -#endif - - ctx->rtp_sockets[CONTROL].rport = pCtrlPort; - ctx->rtp_sockets[TIMING].rport = pTimingPort; - - if (aesiv && aeskey) { - memcpy(ctx->aesiv, aesiv, 16); -#ifdef WIN32 - AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes); -#else - memset(&ctx->aes, 0, sizeof(mbedtls_aes_context)); - mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128); -#endif - ctx->decrypt = true; - ctx->decrypt_buf = malloc(MAX_PACKET); - } - - memset(fmtp, 0, sizeof(fmtp)); - while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg); - - ctx->frame_size = fmtp[1]; - ctx->frame_duration = (ctx->frame_size * 1000) / RAOP_SAMPLE_RATE; - - // alac decoder - ctx->alac_codec = alac_init(fmtp); - rc &= ctx->alac_codec != NULL; - - buffer_alloc(ctx->audio_buffer, ctx->frame_size*4); - - // create rtp ports - for (i = 0; i < 3; i++) { - ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM); - rc &= ctx->rtp_sockets[i].sock > 0; - } - - // create http port and start listening - resp.cport = ctx->rtp_sockets[CONTROL].lport; - resp.tport = ctx->rtp_sockets[TIMING].lport; - resp.aport = ctx->rtp_sockets[DATA].lport; - - if (rc) { - ctx->running = true; -#ifdef WIN32 - pthread_create(&ctx->thread, NULL, rtp_thread_func, (void *) ctx); -#else - // xTaskCreate((TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_TASK_SIZE, ctx, CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1 , &ctx->thread); - ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_STACK_SIZE, ctx, - CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, ctx->xStack, ctx->xTaskBuffer ); -#endif - } else { - LOG_ERROR("[%p]: cannot start RTP", ctx); - rtp_end(ctx); - ctx = NULL; - } - - resp.ctx = ctx; - - return resp; -} - -/*---------------------------------------------------------------------------*/ -void rtp_end(rtp_t *ctx) -{ - int i; - - if (!ctx) return; - - if (ctx->running) { -#if !defined WIN32 - ctx->joiner = xTaskGetCurrentTaskHandle(); -#endif - ctx->running = false; -#ifdef WIN32 - pthread_join(ctx->thread, NULL); -#else - ulTaskNotifyTake(pdFALSE, portMAX_DELAY); - vTaskDelete(ctx->thread); - heap_caps_free(ctx->xTaskBuffer); -#endif - } - - for (i = 0; i < 3; i++) closesocket(ctx->rtp_sockets[i].sock); - - if (ctx->alac_codec) alac_delete_decoder(ctx->alac_codec); - if (ctx->decrypt_buf) free(ctx->decrypt_buf); - - pthread_mutex_destroy(&ctx->ab_mutex); - buffer_release(ctx->audio_buffer); - - free(ctx); - -#ifdef __RTP_STORE - fclose(ctx->rtpIN); - fclose(ctx->rtpOUT); -#endif -} - -/*---------------------------------------------------------------------------*/ -bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime) -{ - bool rc = true; - u32_t now = gettime_ms(); - - if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { - rc = false; - LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); - } else { - pthread_mutex_lock(&ctx->ab_mutex); - buffer_reset(ctx->audio_buffer); - ctx->playing = false; - ctx->flush_seqno = seqno; - pthread_mutex_unlock(&ctx->ab_mutex); - } - - LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); - - return rc; -} - -/*---------------------------------------------------------------------------*/ -void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) -{ - ctx->record.seqno = seqno; - ctx->record.rtptime = rtptime; - ctx->record.time = gettime_ms(); - - LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); -} - -/*---------------------------------------------------------------------------*/ -static void buffer_alloc(abuf_t *audio_buffer, int size) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) { - audio_buffer[i].data = malloc(size); - audio_buffer[i].ready = 0; - } -} - -/*---------------------------------------------------------------------------*/ -static void buffer_release(abuf_t *audio_buffer) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) { - free(audio_buffer[i].data); - } -} - -/*---------------------------------------------------------------------------*/ -static void buffer_reset(abuf_t *audio_buffer) { - int i; - for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0; -} - -/*---------------------------------------------------------------------------*/ -// the sequence numbers will wrap pretty often. -// this returns true if the second arg is after the first -static int seq_order(seq_t a, seq_t b) { - s16_t d = b - a; - return d > 0; -} - -/*---------------------------------------------------------------------------*/ -static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) { - unsigned char iv[16]; - int aeslen; - assert(len<=MAX_PACKET); - - if (ctx->decrypt) { - aeslen = len & ~0xf; - memcpy(iv, ctx->aesiv, sizeof(iv)); -#ifdef WIN32 - AES_cbc_encrypt((unsigned char*)buf, ctx->decrypt_buf, aeslen, &ctx->aes, iv, AES_DECRYPT); -#else - mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, ctx->decrypt_buf); -#endif - memcpy(ctx->decrypt_buf+aeslen, buf+aeslen, len-aeslen); - alac_to_pcm(ctx->alac_codec, (unsigned char*) ctx->decrypt_buf, (unsigned char*) dest, 2, (unsigned int*) outsize); - } else { - alac_to_pcm(ctx->alac_codec, (unsigned char*) buf, (unsigned char*) dest, 2, (unsigned int*) outsize); - } - - *outsize *= 4; -} - - -/*---------------------------------------------------------------------------*/ -static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { - abuf_t *abuf = NULL; - u32_t playtime; - - pthread_mutex_lock(&ctx->ab_mutex); - - if (!ctx->playing) { - if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && - (ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) { - ctx->ab_write = seqno-1; - ctx->ab_read = seqno; - ctx->flush_seqno = -1; - ctx->playing = true; - ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; - playtime = ctx->synchro.time + (((s32_t)(rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; - ctx->cmd_cb(RAOP_PLAY, &playtime); - } else { - pthread_mutex_unlock(&ctx->ab_mutex); - return; - } - } - - if (seqno == (u16_t) (ctx->ab_write+1)) { - // expected packet - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->ab_write = seqno; - LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read); - - } else if (seq_order(ctx->ab_write, seqno)) { - // newer than expected - if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_write - 1)) { - // only get rtp latency-1 frames back (last one is seqno) - LOG_WARN("[%p] too many missing frames %hu seq: %hu, (W:%hu R:%hu)", ctx, seqno - ctx->ab_write - 1, seqno, ctx->ab_write, ctx->ab_read); - ctx->ab_write = seqno - ctx->latency / ctx->frame_size; - } - if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_read)) { - // if ab_read is lagging more than http latency, advance it - LOG_WARN("[%p] on hold for too long %hu (W:%hu R:%hu)", ctx, seqno - ctx->ab_read + 1, ctx->ab_write, ctx->ab_read); - ctx->ab_read = seqno - ctx->latency / ctx->frame_size + 1; - } - if (rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1)) { - seq_t i; - u32_t now = gettime_ms(); - for (i = ctx->ab_write + 1; seq_order(i, seqno); i++) { - ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size; - ctx->audio_buffer[BUFIDX(i)].last_resend = now; - } - } - LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->ab_write = seqno; - } else if (seq_order(ctx->ab_read, seqno + 1)) { - // recovered packet, not yet sent - abuf = ctx->audio_buffer + BUFIDX(seqno); - ctx->resent_rec++; - LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - } else { - // too late - LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); - } - - if (ctx->in_frames++ > 1000) { - LOG_INFO("[%p]: fill [level:%hu rec:%u] [W:%hu R:%hu]", ctx, ctx->ab_write - ctx->ab_read, ctx->resent_rec, ctx->ab_write, ctx->ab_read); - ctx->in_frames = 0; - } - - if (abuf) { - alac_decode(ctx, abuf->data, data, len, &abuf->len); - abuf->ready = 1; - // this is the local rtptime when this frame is expected to play - abuf->rtptime = rtptime; - buffer_push_packet(ctx); - -#ifdef __RTP_STORE - fwrite(data, len, 1, ctx->rtpIN); - fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT); -#endif - } - - pthread_mutex_unlock(&ctx->ab_mutex); -} - -/*---------------------------------------------------------------------------*/ -// push as many frames as possible through callback -static void buffer_push_packet(rtp_t *ctx) { - abuf_t *curframe = NULL; - u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); - int i; - - // not ready to play yet - if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; - - // maybe re-evaluate time in loop in case data callback blocks ... - now = gettime_ms(); - - // there is always at least one frame in the buffer - do { - - curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); - playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; - - if (now > playtime) { - LOG_DEBUG("[%p]: discarded frame now:%u missed by:%d (W:%hu R:%hu)", ctx, now, now - playtime, ctx->ab_write, ctx->ab_read); - ctx->discarded++; - curframe->ready = 0; - } else if (curframe->ready) { - ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); - curframe->ready = 0; - } else if (playtime - now <= hold) { - LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); - ctx->data_cb(silence_frame, ctx->frame_size * 4, playtime); - ctx->silent_frames++; - } else break; - - ctx->ab_read++; - ctx->out_frames++; - - } while (seq_order(ctx->ab_read, ctx->ab_write)); - - if (ctx->out_frames > 1000) { - LOG_INFO("[%p]: drain [level:%hd head:%d ms] [W:%hu R:%hu] [req:%u sil:%u dis:%u]", - ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read, - ctx->resent_req, ctx->silent_frames, ctx->discarded); - ctx->out_frames = 0; - } - - LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); - - // each missing packet will be requested up to (latency_frames / 16) times - for (i = 0; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { - abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); - if (!frame->ready && now - frame->last_resend > RESEND_TO) { - rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); - frame->last_resend = now; - } - } - } - - -/*---------------------------------------------------------------------------*/ -static void *rtp_thread_func(void *arg) { - fd_set fds; - int i, sock = -1; - int count = 0; - bool ntp_sent; - char *packet = malloc(MAX_PACKET); - rtp_t *ctx = (rtp_t*) arg; - - for (i = 0; i < 3; i++) { - if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock; - // send synchro requets 3 times - ntp_sent = rtp_request_timing(ctx); - } - - while (ctx->running) { - ssize_t plen; - char type; - socklen_t rtp_client_len = sizeof(struct sockaddr_storage); - int idx = 0; - char *pktp = packet; - struct timeval timeout = {0, 100*1000}; - - FD_ZERO(&fds); - for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); } - - if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue; - - for (i = 0; i < 3; i++) - if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i; - - plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, 0, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len); - - if (!ntp_sent) { - LOG_WARN("[%p]: NTP request not send yet", ctx); - ntp_sent = rtp_request_timing(ctx); - } - - if (plen < 0) continue; - assert(plen <= MAX_PACKET); - - type = packet[1] & ~0x80; - pktp = packet; - - switch (type) { - seq_t seqno; - unsigned rtptime; - - // re-sent packet - case 0x56: { - pktp += 4; - plen -= 4; - } - - // data packet - case 0x60: { - seqno = ntohs(*(u16_t*)(pktp+2)); - rtptime = ntohl(*(u32_t*)(pktp+4)); - - // adjust pointer and length - pktp += 12; - plen -= 12; - - LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80); - - // check if packet contains enough content to be reasonable - if (plen < 16) break; - - if ((packet[1] & 0x80) && (type != 0x56)) { - LOG_INFO("[%p]: 1st audio packet received", ctx); - } - - buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen); - - break; - } - - // sync packet - case 0x54: { - u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4)); - u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12)); - u32_t rtp_now = ntohl(*(u32_t*)(pktp+16)); - u16_t flags = ntohs(*(u16_t*)(pktp+2)); - u32_t remote_gap = NTP2MS(remote - ctx->timing.remote); - - if (remote_gap > 10000) { - LOG_WARN("discarding remote timing information %u", remote_gap); - break; - } - - pthread_mutex_lock(&ctx->ab_mutex); - - // re-align timestamp and expected local playback time (and magic 11025 latency) - ctx->latency = rtp_now - rtp_now_latency; - if (flags == 7 || flags == 4) ctx->latency += 11025; - if (ctx->latency < MIN_LATENCY) ctx->latency = MIN_LATENCY; - else if (ctx->latency > MAX_LATENCY) ctx->latency = MAX_LATENCY; - ctx->synchro.rtp = rtp_now - ctx->latency; - ctx->synchro.time = ctx->timing.local + remote_gap; - - // now we are synced on RTP frames - ctx->synchro.status |= RTP_SYNC; - - // 1st sync packet received (signals a restart of playback) - if (packet[0] & 0x10) { - LOG_INFO("[%p]: 1st sync packet received", ctx); - } - - pthread_mutex_unlock(&ctx->ab_mutex); - - LOG_DEBUG("[%p]: sync packet latency:%d rtp_latency:%u rtp:%u remote ntp:%llx, local time:%u local rtp:%u (now:%u)", - ctx, ctx->latency, rtp_now_latency, rtp_now, remote, ctx->synchro.time, ctx->synchro.rtp, gettime_ms()); - - if (!count--) { - rtp_request_timing(ctx); - count = 3; - } - - if ((ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) ctx->cmd_cb(RAOP_TIMING, NULL); - - break; - } - - // NTP timing packet - case 0x53: { - u64_t expected; - u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case - u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20)); - u32_t roundtrip = gettime_ms() - reference; - - // better discard sync packets when roundtrip is suspicious - if (roundtrip > 100) { - LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip); - break; - } - - /* - The expected elapsed remote time should be exactly the same as - elapsed local time between the two request, corrected by the - drifting - */ - expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local); - - ctx->timing.remote = remote; - ctx->timing.local = reference; - - // now we are synced on NTP (mutex not needed) - ctx->synchro.status |= NTP_SYNC; - - LOG_DEBUG("[%p]: Timing references local:%llu, remote:%llx (delta:%lld, sum:%lld, adjust:%lld, gaps:%d)", - ctx, ctx->timing.local, ctx->timing.remote); - - break; - } - } - } - - free(packet); - LOG_INFO("[%p]: terminating", ctx); - -#ifndef WIN32 - xTaskNotifyGive(ctx->joiner); - vTaskSuspend(NULL); -#endif - - return NULL; -} - -/*---------------------------------------------------------------------------*/ -static bool rtp_request_timing(rtp_t *ctx) { - unsigned char req[32]; - u32_t now = gettime_ms(); - int i; - struct sockaddr_in host; - - LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport); - - req[0] = 0x80; - req[1] = 0x52|0x80; - *(u16_t*)(req+2) = htons(7); - *(u32_t*)(req+4) = htonl(0); // dummy - for (i = 0; i < 16; i++) req[i+8] = 0; - *(u32_t*)(req+24) = 0; - *(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP - - if (ctx->host.s_addr != INADDR_ANY) { - host.sin_family = AF_INET; - host.sin_addr = ctx->host; - } else host = ctx->rtp_host; - - // no address from sender, need to wait for 1st packet to be received - if (host.sin_addr.s_addr == INADDR_ANY) return false; - - host.sin_port = htons(ctx->rtp_sockets[TIMING].rport); - - if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), 0, (struct sockaddr*) &host, sizeof(host))) { - LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); - } - - return true; -} - -/*---------------------------------------------------------------------------*/ -static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) { - unsigned char req[8]; // *not* a standard RTCP NACK - - // do not request silly ranges (happens in case of network large blackouts) - if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false; - - ctx->resent_req += last - first + 1; - - LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last); - - req[0] = 0x80; - req[1] = 0x55|0x80; // Apple 'resend' - *(u16_t*)(req+2) = htons(1); // our seqnum - *(u16_t*)(req+4) = htons(first); // missed seqnum - *(u16_t*)(req+6) = htons(last-first+1); // count - - ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport); - - if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), 0, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) { - LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); - } - - return true; -} - + +/* + * HairTunes - RAOP packet handler and slave-clocked replay engine + * Copyright (c) James Laird 2011 + * All rights reserved. + * + * Modularisation: philippe_44@outlook.com, 2019 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "rtp.h" +#include "raop_sink.h" +#include "log_util.h" +#include "util.h" + +#ifdef WIN32 +#include +#include "alac_wrapper.h" +#else +#include "esp_pthread.h" +#include "esp_system.h" +#include +#include +#include "alac_wrapper.h" +#endif + +#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22) +#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10) +#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16) +#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16) +#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000) +#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate)) + + extern log_level raop_loglevel; + static log_level *loglevel = &raop_loglevel; + +//#define __RTP_STORE + +// default buffer size +#define BUFFER_FRAMES ( (150 * RAOP_SAMPLE_RATE * 2) / (352 * 100) ) +#define MAX_PACKET 1408 +#define MIN_LATENCY 11025 +#define MAX_LATENCY ( (120 * RAOP_SAMPLE_RATE * 2) / 100 ) + +#define RTP_STACK_SIZE (4*1024) + +#define RTP_SYNC (0x01) +#define NTP_SYNC (0x02) + +#define RESEND_TO 200 + +enum { DATA = 0, CONTROL, TIMING }; + +static const u8_t silence_frame[MAX_PACKET] = { 0 }; + +typedef u16_t seq_t; +typedef struct audio_buffer_entry { // decoded audio packets + int ready; + u32_t rtptime, last_resend; + s16_t *data; + int len; +} abuf_t; + +typedef struct rtp_s { +#ifdef __RTP_STORE + FILE *rtpIN, *rtpOUT; +#endif + bool running; + unsigned char aesiv[16]; +#ifdef WIN32 + AES_KEY aes; +#else + mbedtls_aes_context aes; +#endif + bool decrypt; + u8_t *decrypt_buf; + u32_t frame_size, frame_duration; + u32_t in_frames, out_frames; + struct in_addr host; + struct sockaddr_in rtp_host; + struct { + unsigned short rport, lport; + int sock; + } rtp_sockets[3]; // data, control, timing + struct timing_s { + u64_t local, remote; + } timing; + struct { + u32_t rtp, time; + u8_t status; + } synchro; + struct { + u32_t time; + seq_t seqno; + u32_t rtptime; + } record; + int latency; // rtp hold depth in samples + u32_t resent_req, resent_rec; // total resent + recovered frames + u32_t silent_frames; // total silence frames + u32_t discarded; + abuf_t audio_buffer[BUFFER_FRAMES]; + seq_t ab_read, ab_write; + pthread_mutex_t ab_mutex; +#ifdef WIN32 + pthread_t thread; +#else + TaskHandle_t thread, joiner; + StaticTask_t *xTaskBuffer; + StackType_t xStack[RTP_STACK_SIZE] __attribute__ ((aligned (4))); +#endif + + struct alac_codec_s *alac_codec; + int flush_seqno; + bool playing; + raop_data_cb_t data_cb; + raop_cmd_cb_t cmd_cb; +} rtp_t; + + +#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) +static void buffer_alloc(abuf_t *audio_buffer, int size); +static void buffer_release(abuf_t *audio_buffer); +static void buffer_reset(abuf_t *audio_buffer); +static void buffer_push_packet(rtp_t *ctx); +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last); +static bool rtp_request_timing(rtp_t *ctx); +static void* rtp_thread_func(void *arg); +static int seq_order(seq_t a, seq_t b); + +/*---------------------------------------------------------------------------*/ +static struct alac_codec_s* alac_init(int fmtp[32]) { + struct alac_codec_s *alac; + unsigned sample_rate; + unsigned char sample_size, channels; + struct { + uint32_t frameLength; + uint8_t compatibleVersion; + uint8_t bitDepth; + uint8_t pb; + uint8_t mb; + uint8_t kb; + uint8_t numChannels; + uint16_t maxRun; + uint32_t maxFrameBytes; + uint32_t avgBitRate; + uint32_t sampleRate; + } config; + + config.frameLength = htonl(fmtp[1]); + config.compatibleVersion = fmtp[2]; + config.bitDepth = fmtp[3]; + config.pb = fmtp[4]; + config.mb = fmtp[5]; + config.kb = fmtp[6]; + config.numChannels = fmtp[7]; + config.maxRun = htons(fmtp[8]); + config.maxFrameBytes = htonl(fmtp[9]); + config.avgBitRate = htonl(fmtp[10]); + config.sampleRate = htonl(fmtp[11]); + + alac = alac_create_decoder(sizeof(config), (unsigned char*) &config, &sample_size, &sample_rate, &channels); + if (!alac) { + LOG_ERROR("cannot create alac codec", NULL); + return NULL; + } + + return alac; +} + +/*---------------------------------------------------------------------------*/ +rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, char *fmtpstr, + short unsigned pCtrlPort, short unsigned pTimingPort, + raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) +{ + int i = 0; + char *arg; + int fmtp[12]; + bool rc = true; + rtp_t *ctx = calloc(1, sizeof(rtp_t)); + rtp_resp_t resp = { 0, 0, 0, NULL }; + + if (!ctx) return resp; + + ctx->host = host; + ctx->decrypt = false; + ctx->cmd_cb = cmd_cb; + ctx->data_cb = data_cb; + ctx->rtp_host.sin_family = AF_INET; + ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; + pthread_mutex_init(&ctx->ab_mutex, 0); + ctx->flush_seqno = -1; + ctx->latency = latency; + ctx->ab_read = ctx->ab_write; + +#ifdef __RTP_STORE + ctx->rtpIN = fopen("airplay.rtpin", "wb"); + ctx->rtpOUT = fopen("airplay.rtpout", "wb"); +#endif + + ctx->rtp_sockets[CONTROL].rport = pCtrlPort; + ctx->rtp_sockets[TIMING].rport = pTimingPort; + + if (aesiv && aeskey) { + memcpy(ctx->aesiv, aesiv, 16); +#ifdef WIN32 + AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes); +#else + memset(&ctx->aes, 0, sizeof(mbedtls_aes_context)); + mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128); +#endif + ctx->decrypt = true; + ctx->decrypt_buf = malloc(MAX_PACKET); + } + + memset(fmtp, 0, sizeof(fmtp)); + while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg); + + ctx->frame_size = fmtp[1]; + ctx->frame_duration = (ctx->frame_size * 1000) / RAOP_SAMPLE_RATE; + + // alac decoder + ctx->alac_codec = alac_init(fmtp); + rc &= ctx->alac_codec != NULL; + + buffer_alloc(ctx->audio_buffer, ctx->frame_size*4); + + // create rtp ports + for (i = 0; i < 3; i++) { + ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM); + rc &= ctx->rtp_sockets[i].sock > 0; + } + + // create http port and start listening + resp.cport = ctx->rtp_sockets[CONTROL].lport; + resp.tport = ctx->rtp_sockets[TIMING].lport; + resp.aport = ctx->rtp_sockets[DATA].lport; + + if (rc) { + ctx->running = true; +#ifdef WIN32 + pthread_create(&ctx->thread, NULL, rtp_thread_func, (void *) ctx); +#else + // xTaskCreate((TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_TASK_SIZE, ctx, CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1 , &ctx->thread); + ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ctx->thread = xTaskCreateStatic( (TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_STACK_SIZE, ctx, + CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, ctx->xStack, ctx->xTaskBuffer ); +#endif + } else { + LOG_ERROR("[%p]: cannot start RTP", ctx); + rtp_end(ctx); + ctx = NULL; + } + + resp.ctx = ctx; + + return resp; +} + +/*---------------------------------------------------------------------------*/ +void rtp_end(rtp_t *ctx) +{ + int i; + + if (!ctx) return; + + if (ctx->running) { +#if !defined WIN32 + ctx->joiner = xTaskGetCurrentTaskHandle(); +#endif + ctx->running = false; +#ifdef WIN32 + pthread_join(ctx->thread, NULL); +#else + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + vTaskDelete(ctx->thread); + heap_caps_free(ctx->xTaskBuffer); +#endif + } + + for (i = 0; i < 3; i++) closesocket(ctx->rtp_sockets[i].sock); + + if (ctx->alac_codec) alac_delete_decoder(ctx->alac_codec); + if (ctx->decrypt_buf) free(ctx->decrypt_buf); + + pthread_mutex_destroy(&ctx->ab_mutex); + buffer_release(ctx->audio_buffer); + + free(ctx); + +#ifdef __RTP_STORE + fclose(ctx->rtpIN); + fclose(ctx->rtpOUT); +#endif +} + +/*---------------------------------------------------------------------------*/ +bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime) +{ + bool rc = true; + u32_t now = gettime_ms(); + + if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { + rc = false; + LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); + } else { + pthread_mutex_lock(&ctx->ab_mutex); + buffer_reset(ctx->audio_buffer); + ctx->playing = false; + ctx->flush_seqno = seqno; + pthread_mutex_unlock(&ctx->ab_mutex); + } + + LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); + + return rc; +} + +/*---------------------------------------------------------------------------*/ +void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) +{ + ctx->record.seqno = seqno; + ctx->record.rtptime = rtptime; + ctx->record.time = gettime_ms(); + + LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); +} + +/*---------------------------------------------------------------------------*/ +static void buffer_alloc(abuf_t *audio_buffer, int size) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + audio_buffer[i].data = malloc(size); + audio_buffer[i].ready = 0; + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_release(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) { + free(audio_buffer[i].data); + } +} + +/*---------------------------------------------------------------------------*/ +static void buffer_reset(abuf_t *audio_buffer) { + int i; + for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0; +} + +/*---------------------------------------------------------------------------*/ +// the sequence numbers will wrap pretty often. +// this returns true if the second arg is after the first +static int seq_order(seq_t a, seq_t b) { + s16_t d = b - a; + return d > 0; +} + +/*---------------------------------------------------------------------------*/ +static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) { + unsigned char iv[16]; + int aeslen; + assert(len<=MAX_PACKET); + + if (ctx->decrypt) { + aeslen = len & ~0xf; + memcpy(iv, ctx->aesiv, sizeof(iv)); +#ifdef WIN32 + AES_cbc_encrypt((unsigned char*)buf, ctx->decrypt_buf, aeslen, &ctx->aes, iv, AES_DECRYPT); +#else + mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, ctx->decrypt_buf); +#endif + memcpy(ctx->decrypt_buf+aeslen, buf+aeslen, len-aeslen); + alac_to_pcm(ctx->alac_codec, (unsigned char*) ctx->decrypt_buf, (unsigned char*) dest, 2, (unsigned int*) outsize); + } else { + alac_to_pcm(ctx->alac_codec, (unsigned char*) buf, (unsigned char*) dest, 2, (unsigned int*) outsize); + } + + *outsize *= 4; +} + + +/*---------------------------------------------------------------------------*/ +static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { + abuf_t *abuf = NULL; + u32_t playtime; + + pthread_mutex_lock(&ctx->ab_mutex); + + if (!ctx->playing) { + if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && + (ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) { + ctx->ab_write = seqno-1; + ctx->ab_read = seqno; + ctx->flush_seqno = -1; + ctx->playing = true; + ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; + playtime = ctx->synchro.time + (((s32_t)(rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; + ctx->cmd_cb(RAOP_PLAY, &playtime); + } else { + pthread_mutex_unlock(&ctx->ab_mutex); + return; + } + } + + if (seqno == (u16_t) (ctx->ab_write+1)) { + // expected packet + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read); + + } else if (seq_order(ctx->ab_write, seqno)) { + // newer than expected + if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_write - 1)) { + // only get rtp latency-1 frames back (last one is seqno) + LOG_WARN("[%p] too many missing frames %hu seq: %hu, (W:%hu R:%hu)", ctx, seqno - ctx->ab_write - 1, seqno, ctx->ab_write, ctx->ab_read); + ctx->ab_write = seqno - ctx->latency / ctx->frame_size; + } + if (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_read)) { + // if ab_read is lagging more than http latency, advance it + LOG_WARN("[%p] on hold for too long %hu (W:%hu R:%hu)", ctx, seqno - ctx->ab_read + 1, ctx->ab_write, ctx->ab_read); + ctx->ab_read = seqno - ctx->latency / ctx->frame_size + 1; + } + if (rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1)) { + seq_t i; + u32_t now = gettime_ms(); + for (i = ctx->ab_write + 1; seq_order(i, seqno); i++) { + ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size; + ctx->audio_buffer[BUFIDX(i)].last_resend = now; + } + } + LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->ab_write = seqno; + } else if (seq_order(ctx->ab_read, seqno + 1)) { + // recovered packet, not yet sent + abuf = ctx->audio_buffer + BUFIDX(seqno); + ctx->resent_rec++; + LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } else { + // too late + LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read); + } + + if (ctx->in_frames++ > 1000) { + LOG_INFO("[%p]: fill [level:%hu rec:%u] [W:%hu R:%hu]", ctx, ctx->ab_write - ctx->ab_read, ctx->resent_rec, ctx->ab_write, ctx->ab_read); + ctx->in_frames = 0; + } + + if (abuf) { + alac_decode(ctx, abuf->data, data, len, &abuf->len); + abuf->ready = 1; + // this is the local rtptime when this frame is expected to play + abuf->rtptime = rtptime; + buffer_push_packet(ctx); + +#ifdef __RTP_STORE + fwrite(data, len, 1, ctx->rtpIN); + fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT); +#endif + } + + pthread_mutex_unlock(&ctx->ab_mutex); +} + +/*---------------------------------------------------------------------------*/ +// push as many frames as possible through callback +static void buffer_push_packet(rtp_t *ctx) { + abuf_t *curframe = NULL; + u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); + int i; + + // not ready to play yet + if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; + + // maybe re-evaluate time in loop in case data callback blocks ... + now = gettime_ms(); + + // there is always at least one frame in the buffer + do { + + curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read); + playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / RAOP_SAMPLE_RATE; + + if (now > playtime) { + LOG_DEBUG("[%p]: discarded frame now:%u missed by:%d (W:%hu R:%hu)", ctx, now, now - playtime, ctx->ab_write, ctx->ab_read); + ctx->discarded++; + curframe->ready = 0; + } else if (curframe->ready) { + ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime); + curframe->ready = 0; + } else if (playtime - now <= hold) { + LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read); + ctx->data_cb(silence_frame, ctx->frame_size * 4, playtime); + ctx->silent_frames++; + } else break; + + ctx->ab_read++; + ctx->out_frames++; + + } while (seq_order(ctx->ab_read, ctx->ab_write)); + + if (ctx->out_frames > 1000) { + LOG_INFO("[%p]: drain [level:%hd head:%d ms] [W:%hu R:%hu] [req:%u sil:%u dis:%u]", + ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read, + ctx->resent_req, ctx->silent_frames, ctx->discarded); + ctx->out_frames = 0; + } + + LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready); + + // each missing packet will be requested up to (latency_frames / 16) times + for (i = 0; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) { + abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i); + if (!frame->ready && now - frame->last_resend > RESEND_TO) { + rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i); + frame->last_resend = now; + } + } + } + + +/*---------------------------------------------------------------------------*/ +static void *rtp_thread_func(void *arg) { + fd_set fds; + int i, sock = -1; + int count = 0; + bool ntp_sent; + char *packet = malloc(MAX_PACKET); + rtp_t *ctx = (rtp_t*) arg; + + for (i = 0; i < 3; i++) { + if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock; + // send synchro requets 3 times + ntp_sent = rtp_request_timing(ctx); + } + + while (ctx->running) { + ssize_t plen; + char type; + socklen_t rtp_client_len = sizeof(struct sockaddr_storage); + int idx = 0; + char *pktp = packet; + struct timeval timeout = {0, 100*1000}; + + FD_ZERO(&fds); + for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); } + + if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue; + + for (i = 0; i < 3; i++) + if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i; + + plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, 0, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len); + + if (!ntp_sent) { + LOG_WARN("[%p]: NTP request not send yet", ctx); + ntp_sent = rtp_request_timing(ctx); + } + + if (plen < 0) continue; + assert(plen <= MAX_PACKET); + + type = packet[1] & ~0x80; + pktp = packet; + + switch (type) { + seq_t seqno; + unsigned rtptime; + + // re-sent packet + case 0x56: { + pktp += 4; + plen -= 4; + } + + // data packet + case 0x60: { + seqno = ntohs(*(u16_t*)(pktp+2)); + rtptime = ntohl(*(u32_t*)(pktp+4)); + + // adjust pointer and length + pktp += 12; + plen -= 12; + + LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80); + + // check if packet contains enough content to be reasonable + if (plen < 16) break; + + if ((packet[1] & 0x80) && (type != 0x56)) { + LOG_INFO("[%p]: 1st audio packet received", ctx); + } + + buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen); + + break; + } + + // sync packet + case 0x54: { + u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4)); + u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12)); + u32_t rtp_now = ntohl(*(u32_t*)(pktp+16)); + u16_t flags = ntohs(*(u16_t*)(pktp+2)); + u32_t remote_gap = NTP2MS(remote - ctx->timing.remote); + + if (remote_gap > 10000) { + LOG_WARN("discarding remote timing information %u", remote_gap); + break; + } + + pthread_mutex_lock(&ctx->ab_mutex); + + // re-align timestamp and expected local playback time (and magic 11025 latency) + ctx->latency = rtp_now - rtp_now_latency; + if (flags == 7 || flags == 4) ctx->latency += 11025; + if (ctx->latency < MIN_LATENCY) ctx->latency = MIN_LATENCY; + else if (ctx->latency > MAX_LATENCY) ctx->latency = MAX_LATENCY; + ctx->synchro.rtp = rtp_now - ctx->latency; + ctx->synchro.time = ctx->timing.local + remote_gap; + + // now we are synced on RTP frames + ctx->synchro.status |= RTP_SYNC; + + // 1st sync packet received (signals a restart of playback) + if (packet[0] & 0x10) { + LOG_INFO("[%p]: 1st sync packet received", ctx); + } + + pthread_mutex_unlock(&ctx->ab_mutex); + + LOG_DEBUG("[%p]: sync packet latency:%d rtp_latency:%u rtp:%u remote ntp:%llx, local time:%u local rtp:%u (now:%u)", + ctx, ctx->latency, rtp_now_latency, rtp_now, remote, ctx->synchro.time, ctx->synchro.rtp, gettime_ms()); + + if (!count--) { + rtp_request_timing(ctx); + count = 3; + } + + if ((ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) ctx->cmd_cb(RAOP_TIMING, NULL); + + break; + } + + // NTP timing packet + case 0x53: { + u64_t expected; + u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case + u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20)); + u32_t roundtrip = gettime_ms() - reference; + + // better discard sync packets when roundtrip is suspicious + if (roundtrip > 100) { + LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip); + break; + } + + /* + The expected elapsed remote time should be exactly the same as + elapsed local time between the two request, corrected by the + drifting + */ + expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local); + + ctx->timing.remote = remote; + ctx->timing.local = reference; + + // now we are synced on NTP (mutex not needed) + ctx->synchro.status |= NTP_SYNC; + + LOG_DEBUG("[%p]: Timing references local:%llu, remote:%llx (delta:%lld, sum:%lld, adjust:%lld, gaps:%d)", + ctx, ctx->timing.local, ctx->timing.remote); + + break; + } + } + } + + free(packet); + LOG_INFO("[%p]: terminating", ctx); + +#ifndef WIN32 + xTaskNotifyGive(ctx->joiner); + vTaskSuspend(NULL); +#endif + + return NULL; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_timing(rtp_t *ctx) { + unsigned char req[32]; + u32_t now = gettime_ms(); + int i; + struct sockaddr_in host; + + LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport); + + req[0] = 0x80; + req[1] = 0x52|0x80; + *(u16_t*)(req+2) = htons(7); + *(u32_t*)(req+4) = htonl(0); // dummy + for (i = 0; i < 16; i++) req[i+8] = 0; + *(u32_t*)(req+24) = 0; + *(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP + + if (ctx->host.s_addr != INADDR_ANY) { + host.sin_family = AF_INET; + host.sin_addr = ctx->host; + } else host = ctx->rtp_host; + + // no address from sender, need to wait for 1st packet to be received + if (host.sin_addr.s_addr == INADDR_ANY) return false; + + host.sin_port = htons(ctx->rtp_sockets[TIMING].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), 0, (struct sockaddr*) &host, sizeof(host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + +/*---------------------------------------------------------------------------*/ +static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) { + unsigned char req[8]; // *not* a standard RTCP NACK + + // do not request silly ranges (happens in case of network large blackouts) + if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false; + + ctx->resent_req += last - first + 1; + + LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last); + + req[0] = 0x80; + req[1] = 0x55|0x80; // Apple 'resend' + *(u16_t*)(req+2) = htons(1); // our seqnum + *(u16_t*)(req+4) = htons(first); // missed seqnum + *(u16_t*)(req+6) = htons(last-first+1); // count + + ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport); + + if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), 0, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) { + LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno)); + } + + return true; +} + diff --git a/components/services/battery.c b/components/services/battery.c index b4388a09..8406ef78 100644 --- a/components/services/battery.c +++ b/components/services/battery.c @@ -21,13 +21,11 @@ static const char TAG[] = "battery"; -#ifdef CONFIG_SQUEEZEAMP static struct { float sum, avg; int count; TimerHandle_t timer; } battery; -#endif /**************************************************************************************** * diff --git a/components/services/buttons.c b/components/services/buttons.c index 56123615..f60509de 100644 --- a/components/services/buttons.c +++ b/components/services/buttons.c @@ -156,7 +156,7 @@ void button_create(void *id, int gpio, int type, bool pull, int debounce, button if (n_buttons >= MAX_BUTTONS) return; - ESP_LOGI(TAG, "creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %u", gpio, type, pull, long_press, shifter_gpio); + ESP_LOGI(TAG, "Creating button using GPIO %u, type %u, pull-up/down %u, long press %u shifter %u", gpio, type, pull, long_press, shifter_gpio); if (!n_buttons) { button_evt_queue = xQueueCreate(10, sizeof(struct button_s)); diff --git a/components/squeezelite/embedded.h b/components/squeezelite/embedded.h index e604fdfa..b0fad3e5 100644 --- a/components/squeezelite/embedded.h +++ b/components/squeezelite/embedded.h @@ -8,6 +8,7 @@ - pthread_create_name - stack size - s16_t, s32_t, s64_t and u64_t + - PLAYER_ID can overload (use #define) - exit - gettime_ms @@ -25,6 +26,7 @@ #define OUTPUT_THREAD_STACK_SIZE 6 * 1024 #define IR_THREAD_STACK_SIZE 6 * 1024 +#define PLAYER_ID 100 #define BASE_CAP "Model=squeezeesp32,AccuratePlayPoints=1,HasDigitalOut=1,HasPolarityInversion=1,Firmware=" VERSION #define EXT_BSS __attribute__((section(".ext_ram.bss"))) @@ -42,13 +44,15 @@ uint32_t _gettime_ms_(void); int pthread_create_name(pthread_t *thread, _CONST pthread_attr_t *attr, void *(*start_routine)( void * ), void *arg, char *name); - + +// must provide of #define as empty macros void embedded_init(void); void register_external(void); void deregister_external(void); void decode_resume(int external); -void vfd_data(u8_t *data, int len); +// optional, please chain +bool (*slimp_handler)(u8_t *data, int len); void (*server_notify)(in_addr_t ip, u16_t hport, u16_t cport); #endif // EMBEDDED_H diff --git a/components/squeezelite/slimproto.c b/components/squeezelite/slimproto.c index c74db311..b53a1961 100644 --- a/components/squeezelite/slimproto.c +++ b/components/squeezelite/slimproto.c @@ -45,7 +45,7 @@ static sockfd sock = -1; static in_addr_t slimproto_ip = 0; static u16_t slimproto_hport = 9000; static u16_t slimproto_cport = 9090; -static u8_t player_id = 100; // squeezeesp32 +static u8_t player_id = PLAYER_ID; extern struct buffer *streambuf; extern struct buffer *outputbuf; @@ -450,11 +450,6 @@ static void process_dsco(u8_t *pkt, int len) { player_id = 12; } -static void process_vfdc(u8_t *pkt, int len) { - LOG_DEBUG("VFDC %u", len); - vfd_data( pkt, len - 4); -} - static void process_setd(u8_t *pkt, int len) { struct setd_packet *setd = (struct setd_packet *)pkt; @@ -531,7 +526,6 @@ static struct handler handlers[] = { { "setd", process_setd }, { "serv", process_serv }, { "dsco", process_dsco }, - { "vfdc", process_vfdc }, { "", NULL }, }; @@ -542,7 +536,7 @@ static void process(u8_t *pack, int len) { if (h->handler) { LOG_DEBUG("%s", h->opcode); h->handler(pack, len); - } else { + } else if (!slimp_handler || !(*slimp_handler)(pack, len)) { pack[4] = '\0'; LOG_WARN("unhandled %s", (char *)pack); } diff --git a/main/Params.txt b/main/Params.txt index 69af8342..9f47df55 100644 --- a/main/Params.txt +++ b/main/Params.txt @@ -1,3 +1,5 @@ +espcoredump.py info_corefile -t b64 -c + LWIP Enable Copy between L2 and L3 no (no) Default receive window size 32768 (5740) @@ -26,21 +28,42 @@ nvs_set autoexec1 str -v "join " # enable nvs_set autoexec u8 -v 1 -nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -m ESP32-BT -R -Z 96000 -r \"44100-44100\" -e flac" -nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -m ESP32-BT -R -Z 96000 -r \"44100-44100\"" +nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -BT -R -Z 96000 -r \"44100-44100\" -e flac" +nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -BT -R -Z 96000 -r \"44100-44100\"" nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -R -Z 96000 -r \"44100-44100\"" -nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -m ESP32-BT -R -u m -Z 192000 -r \"44100-44100\" -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -m ESP32-BT -r \"44100-44100\" -e flac" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32 -R -Z 96000 -r \"44100-44100\" -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32 -R -Z 96000 -r \"96000-96000\" -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32 -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -m ESP32 -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32 -s 192.168.2.10" -nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d decode=info -m ESP32 -s 192.168.2.144" -nvs_set autoexec2 str -v "squeezelite -o SPDIF -b 500:2000 -d all=info -m ESP32" +nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -BT -R -u m -Z 192000 -r \"44100-44100\" -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'RIVAARENA'\" -b 500:2000 -d all=info -BT -r \"44100-44100\" -e flac" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -R -Z 96000 -r \"44100-44100\" -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -R -Z 96000 -r \"96000-96000\" -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -s 192.168.2.10" +nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d decode=info -s 192.168.2.144" +nvs_set autoexec2 str -v "squeezelite -o SPDIF -b 500:2000 -d all=info " + +nvs_set autoexec1 str -v "squeezelite -o I2S -b 500:2000 -d all=info -s 192.168.2.144" +nvs_set autoexec1 str -v "squeezelite -o SPDIF -b 500:2000 -d all=info -s 192.168.2.144" curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=arm64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" sudo add-apt-repository "deb [arch=armhf] https://download.docker.com/linux/debian $(lsb_release -cs) stable" sudo apt-get install docker-ce docker-ce-cli containerd.io + + + +# Build recovery.bin, bootloader.bin, ota_data_initial.bin, partitions.bin +# force appropriate rebuild by touching all the files which may have a RECOVERY_APPLICATION specific source compile logic + find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f -print0 | xargs -0 grep -l "RECOVERY_APPLICATION" | xargs touch + export PROJECT_NAME="recovery" + make -j4 all EXTRA_CPPFLAGS='-DRECOVERY_APPLICATION=1' +make flash +# +# Build squeezelite.bin +# Now force a rebuild by touching all the files which may have a RECOVERY_APPLICATION specific source compile logic +find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f -print0 | xargs -0 grep -l "RECOVERY_APPLICATION" | xargs touch +export PROJECT_NAME="squeezelite" +make -j4 app EXTRA_CPPFLAGS='-DRECOVERY_APPLICATION=0' +python ${IDF_PATH}/components/esptool_py/esptool/esptool.py --chip esp32 --port ${ESPPORT} --baud 2000000 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x150000 ./build/squeezelite.bin +# monitor serial output +make monitor \ No newline at end of file diff --git a/main/esp_app_main.c b/main/esp_app_main.c index 1298e1f4..73e18ae8 100644 --- a/main/esp_app_main.c +++ b/main/esp_app_main.c @@ -71,6 +71,7 @@ extern const uint8_t server_cert_pem_start[] asm("_binary_github_pem_start"); extern const uint8_t server_cert_pem_end[] asm("_binary_github_pem_end"); extern void services_init(void); +extern void display_init(void); /* brief this is an exemple of a callback that you can setup in your own app to get notified of wifi manager event */ void cb_connection_got_ip(void *pvParameter){