Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions html/management.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@
}

.coverimage-container {
width: 80%;
max-width: 400px;
height: 15%;
object-fit: cover;
max-width: 100%;
height: 300px;
margin: auto;
display: block;
}
Expand Down
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extra_scripts =
pre:updateSdkConfig.py
pre:processHtml.py
lib_deps =
https://github.com/schreibfaul1/ESP32-audioI2S.git#6b8264ccafad74e009b3edcc70ae3d4870a6be66 ; v3.4.2+ set time offset
https://github.com/schreibfaul1/ESP32-audioI2S.git#8fdc0317378267e2eb0600ed811ed13c6c23b90c
https://github.com/madhephaestus/ESP32Encoder.git#2c986e08961458454f64010e8d1a7d150d6d87a6
https://github.com/peterus/ESP-FTP-Server-Lib.git#554959f65c04a2ed6d3443f628e76ca7980355ec
https://github.com/FastLED/FastLED.git#3d1f9fe373b295254354f6266030557a497594cf ; v3.10.2
Expand Down
194 changes: 113 additions & 81 deletions src/AudioPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ static void AudioPlayer_SortPlaylist(Playlist *playlist);
static void AudioPlayer_RandomizePlaylist(Playlist *playlist);
static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed);
static void AudioPlayer_ClearCover(void);
static void audio_id3image(File &file, const size_t pos, const size_t size);
static void audio_oggimage(File &file, std::vector<uint32_t> v);

void Audio_TaskPause(void) {
bool audio_active = false;
Expand All @@ -99,6 +101,114 @@ void Audio_TaskResume(void) {
bool audio_active = true;
}

void Audio_InfoCallback(Audio::msg_t m) {
switch (m.e) {
case Audio::evt_info: {
// Log_Printf(LOGLEVEL_INFO, "info: %s", m.msg); // disabled to reduce log especially from files with numerous comments
if (startsWith((char *) m.msg, "slow stream, dropouts")) {
// websocket notify for slow stream
Web_SendWebsocketData(0, WebsocketCodeType::Dropout);
}
break;
}
case Audio::evt_eof: { // end of file
Log_Printf(LOGLEVEL_INFO, "end of file: %s", m.msg);
gPlayProperties.trackFinished = true;
break;
}
case Audio::evt_bitrate: {
Log_Printf(LOGLEVEL_INFO, "bitrate: %s", m.msg);
break;
}
case Audio::evt_icyurl: {
Log_Printf(LOGLEVEL_INFO, "icy URL: %s", m.msg);
if (m.msg && m.msg[0] != '\0' && AudioPlayer_StationLogoUrl.isEmpty()) {
// has station homepage, get favicon url
AudioPlayer_StationLogoUrl = "https://www.google.com/s2/favicons?sz=256&domain_url=" + String(m.msg);
// websocket and mqtt notify station logo has changed
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
}
break;
}
case Audio::evt_id3data: {
if (!m.msg) break;
// Log_Printf(LOGLEVEL_INFO, "ID3 data: %s", m.msg); // disabled to prevent log spam from files with numerous metadata
// get title
if (startsWith((char *) m.msg, "Title") || startsWith((char *) m.msg, "TITLE=") || startsWith((char *) m.msg, "title=")) { // ID3v1, ID3v2.3 and ID3v2.4: "Title:", VORBISCOMMENT: "TITLE=", "title=", "Title="
int titleStart = 6;
if (m.msg[5] == '/') { // ID3v2.2 "Title/Songname/Content description:"
titleStart = 36;
}
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), m.msg + titleStart);
} else {
Audio_setTitle("%s", m.msg + titleStart);
}
}
break;
}
case Audio::evt_lasthost: { // stream URL played
Log_Printf(LOGLEVEL_INFO, "last URL: %s", m.msg);
break;
}
case Audio::evt_name: { // station name or icy-name
Log_Printf(LOGLEVEL_NOTICE, "station name: %s", m.msg);
if (m.msg && m.msg[0] != '\0') {
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), m.msg);
} else {
Audio_setTitle("%s", m.msg);
}
}
break;
}
case Audio::evt_streamtitle: {
if (!gPlayProperties.isWebstream) break; // prevents overwriting correct title for local files
Log_Printf(LOGLEVEL_INFO, "stream title: %s", m.msg);
if (m.msg && m.msg[0] != '\0') {
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), m.msg);
} else {
Audio_setTitle("%s", m.msg);
}
}
break;
}
case Audio::evt_icylogo: { // logo
Log_Printf(LOGLEVEL_INFO, "icy logo: %s", m.msg);
if (m.msg && m.msg[0] != '\0') {
AudioPlayer_StationLogoUrl = m.msg;
// websocket and mqtt notify station logo has changed
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
}
break;
}
case Audio::evt_image: {
if (!gPlayProperties.playlist || gPlayProperties.currentTrackNumber >= gPlayProperties.playlist->size()) {
break;
}
const char *fileName = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber);
File file = gFSystem.open(fileName, FILE_READ);
if (!file) {
Log_Printf(LOGLEVEL_ERROR, "Failed to open file: %s", fileName);
break;
}
char fileType[4];
if (file.readBytes(fileType, 4) == 4) {
if (strncmp(fileType, "OggS", 4) == 0) {
audio_oggimage(file, m.vec);
} else {
audio_id3image(file, m.vec[0], m.vec[1]);
}
}
file.close();
break;
}
default: // ignored events: evt_icydescription, evt_lyrics, evt_log
break;
}
}

void AudioPlayer_Init(void) {
// create audio object
#ifdef BOARD_HAS_PSRAM
Expand Down Expand Up @@ -210,6 +320,7 @@ void AudioPlayer_Init(void) {
gPrefsSettings.getChar("gainHighPass", 0));

audio->setAudioTaskCore(1);
audio->audio_info_callback = Audio_InfoCallback;

audio_active = true;
}
Expand Down Expand Up @@ -927,7 +1038,7 @@ void AudioPlayer_Loop() {
// we check for timeout
if (noAudio && timeout) {
// Audio playback timed out, move on to the next
// System_IndicateError();
System_IndicateError();
gPlayProperties.trackFinished = true;
playbackTimeoutStart = millis();
}
Expand Down Expand Up @@ -1342,85 +1453,6 @@ void AudioPlayer_ClearCover(void) {
#endif
}

// Some mp3-lib-stuff (slightly changed from default)
void audio_info(const char *info) {
Log_Printf(LOGLEVEL_INFO, "info : %s", info);
if (startsWith((char *) info, "slow stream, dropouts")) {
// websocket notify for slow stream
Web_SendWebsocketData(0, WebsocketCodeType::Dropout);
}
}

void audio_id3data(const char *info) { // id3 metadata
Log_Printf(LOGLEVEL_INFO, "id3data : %s", info);
// get title
if (startsWith((char *) info, "Title") || startsWith((char *) info, "TITLE=") || startsWith((char *) info, "title=")) { // ID3: "Title:", VORBISCOMMENT: "TITLE=", "title=", "Title="
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), info + 6);
} else {
Audio_setTitle("%s", info + 6);
}
}
}

void audio_eof_mp3(const char *info) { // end of file
Log_Printf(LOGLEVEL_INFO, "eof_mp3 : %s", info);
gPlayProperties.trackFinished = true;
}

void audio_showstation(const char *info) {
Log_Printf(LOGLEVEL_NOTICE, "station : %s", info);
if (strcmp(info, "")) {
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), info);
} else {
Audio_setTitle("%s", info);
}
}
}

void audio_showstreamtitle(const char *info) {
Log_Printf(LOGLEVEL_INFO, "streamtitle : %s", info);
if (strcmp(info, "")) {
if (gPlayProperties.playlist->size() > 1) {
Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size(), info);
} else {
Audio_setTitle("%s", info);
}
}
}

void audio_bitrate(const char *info) {
Log_Printf(LOGLEVEL_INFO, "bitrate : %s", info);
}

void audio_commercial(const char *info) { // duration in sec
Log_Printf(LOGLEVEL_INFO, "commercial : %s", info);
}

void audio_icyurl(const char *info) { // homepage
Log_Printf(LOGLEVEL_INFO, "icyurl : %s", info);
if ((String(info) != "") && (AudioPlayer_StationLogoUrl == "")) {
// has station homepage, get favicon url
AudioPlayer_StationLogoUrl = "https://www.google.com/s2/favicons?sz=256&domain_url=" + String(info);
// websocket and mqtt notify station logo has changed
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
}
}

void audio_icylogo(const char *info) { // logo
Log_Printf(LOGLEVEL_INFO, "icylogo : %s", info);
if (String(info) != "") {
AudioPlayer_StationLogoUrl = info;
// websocket and mqtt notify station logo has changed
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
}
}

void audio_lasthost(const char *info) { // stream URL played
Log_Printf(LOGLEVEL_INFO, "lasthost : %s", info);
}

// id3 tag: save cover image
void audio_id3image(File &file, const size_t pos, const size_t size) {
// save cover image position and size for later use
Expand Down Expand Up @@ -1488,7 +1520,7 @@ void audio_oggimage(File &file, std::vector<uint32_t> v) {
gFSystem.rename(tmpDecodedCover, decodedCover);
Log_Printf(LOGLEVEL_DEBUG, "Cover decoded and cached in %s", decodedCover.c_str());
}
gPlayProperties.coverFilePos = 1; // flacMarker gives 4 Bytes before METADATA_BLOCK_PICTURE, whereas for flac files audioI2S points 3 Bytes before METADATA_BLOCK_PICTURE, so gPlayProperties.coverFilePos has to be set to 4-3=1
gPlayProperties.coverFilePos = 4; // flacMarker gives 4 Bytes before METADATA_BLOCK_PICTURE (audioI2S points to METADATA_BLOCK_PICTURE since 6241daa)
// websocket and mqtt notify cover image has changed
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
#ifdef MQTT_ENABLE
Expand Down
53 changes: 35 additions & 18 deletions src/Web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,7 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) {
}
return;
}
if (gPlayProperties.currentTrackNumber >= gPlayProperties.playlist->size()) return;
const char *coverFileName = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber);
String decodedCover = "/.cache";
decodedCover.concat(coverFileName);
Expand All @@ -2381,38 +2382,48 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) {
} else {
coverFile = gFSystem.open(coverFileName, FILE_READ);
}
char mimeType[255] {0};
char mimeType[256] {0};
char fileType[4];
coverFile.readBytes(fileType, 4);
if (strncmp(fileType, "ID3", 3) == 0) { // mp3 (ID3v2) Routine
// seek to start position
coverFile.seek(gPlayProperties.coverFilePos);
uint8_t encoding = coverFile.read();
// mime-type (null terminated)
for (uint8_t i = 0u; i < 255; i++) {
mimeType[i] = coverFile.read();
if (uint8_t(mimeType[i]) == 0) {
break;
if (fileType[3] == 0x02) {
// image format (3 Bytes) for ID3v2.2
coverFile.readBytes(mimeType, 3);
if (strcmp(mimeType, "JPG") == 0) strcpy(mimeType, "image/jpeg");
else if (strcmp(mimeType, "PNG") == 0) strcpy(mimeType, "image/png");
else if (strcmp(mimeType, "-->") != 0) strcpy(mimeType, "application/octet-stream");
} else {
// mime-type (null terminated) for ID3v2.3 and ID3v2.4
for (uint8_t i = 0u; i < 255; i++) {
mimeType[i] = coverFile.read();
if (uint8_t(mimeType[i]) == 0) {
break;
}
}
}
// skip image type (1 Byte)
coverFile.read();
// skip description (null terminated)
for (uint8_t i = 0u; i < 255; i++) {
if (uint8_t(coverFile.read()) == 0) {
break;
}
}
// UTF-16 and UTF-16BE are terminated with an extra 0
if (encoding == 1 || encoding == 2) {
coverFile.read();
if (encoding == 0 || encoding == 3) { // ISO-8859-1 and UTF-8: 00 terminated
while (coverFile.read() != 0) {}
} else if (encoding == 1 || encoding == 2) { // UTF-16 and UTF-16BE: 00 00 terminated
while ((coverFile.read() | (coverFile.read() << 8)) != 0) {}
}
} else if (strncmp(fileType, "fLaC", 4) == 0) { // flac Routine
uint32_t length = 0; // length of strings: MIME type, description of the picture, binary picture data
coverFile.seek(gPlayProperties.coverFilePos + 7); // pass cover filesize (3 Bytes) and picture type (4 Bytes)
coverFile.seek(gPlayProperties.coverFilePos + 4); // pass only picture type (4 Bytes) (audioI2S points to METADATA_BLOCK_PICTURE since 6241daa)
for (int i = 0; i < 4; ++i) { // length of mime type string
length = (length << 8) | coverFile.read();
}
if (length > 255) {
Log_Printf(LOGLEVEL_ERROR, "Unexpected MIME type string length (%u > 255). Possible corrupted cover image or wrong coverFilePos (%u). Aborting extraction.", length, gPlayProperties.coverFilePos);
request->send(200, "image/svg+xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg width=\"1792\" height=\"1792\" viewBox=\"0 0 1792 1792\" transform=\"scale (0.6)\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M1664 224v1120q0 50-34 89t-86 60.5-103.5 32-96.5 10.5-96.5-10.5-103.5-32-86-60.5-34-89 34-89 86-60.5 103.5-32 96.5-10.5q105 0 192 39v-537l-768 237v709q0 50-34 89t-86 60.5-103.5 32-96.5 10.5-96.5-10.5-103.5-32-86-60.5-34-89 34-89 86-60.5 103.5-32 96.5-10.5q105 0 192 39v-967q0-31 19-56.5t49-35.5l832-256q12-4 28-4 40 0 68 28t28 68z\"/></svg>");
coverFile.close();
return;
}
for (uint8_t i = 0u; i < length; i++) {
mimeType[i] = coverFile.read();
}
Expand All @@ -2434,10 +2445,16 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) {
coverFile.seek(8);
coverFile.readBytes(fileType, 3);
if (strncmp(fileType, "M4A", 3) == 0) {
// M4A header found, seek to image start position. Image length adjustment seems to be not needed, every browser shows cover image correct!
coverFile.seek(gPlayProperties.coverFilePos + 8);
strcpy(mimeType, "application/octet-stream");
coverFile.seek(gPlayProperties.coverFilePos);
}
}
if (strncmp(mimeType, "image", 5) != 0 && strncmp(mimeType, "application/octet-stream", 24) != 0) {
Log_Printf(LOGLEVEL_ERROR, "Unexpected MIME type (%s). Possible corrupted cover image or wrong coverFilePos (%u). Aborting extraction.", mimeType, gPlayProperties.coverFilePos);
request->send(200, "image/svg+xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg width=\"1792\" height=\"1792\" viewBox=\"0 0 1792 1792\" transform=\"scale (0.6)\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M1664 224v1120q0 50-34 89t-86 60.5-103.5 32-96.5 10.5-96.5-10.5-103.5-32-86-60.5-34-89 34-89 86-60.5 103.5-32 96.5-10.5q105 0 192 39v-537l-768 237v709q0 50-34 89t-86 60.5-103.5 32-96.5 10.5-96.5-10.5-103.5-32-86-60.5-34-89 34-89 86-60.5 103.5-32 96.5-10.5q105 0 192 39v-967q0-31 19-56.5t49-35.5l832-256q12-4 28-4 40 0 68 28t28 68z\"/></svg>");
coverFile.close();
return;
}
Log_Printf(LOGLEVEL_NOTICE, "serve cover image (%s): %s", mimeType, coverFile.name());

int imageSize = gPlayProperties.coverFileSize;
Expand All @@ -2457,6 +2474,6 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) {
index += willWrite;
return willWrite;
});
response->addHeader("Cache Control", "no-cache, must-revalidate");
response->addHeader("Cache-Control", "no-cache, must-revalidate");
request->send(response);
}