From 9f8263a565e09558384bed51576121d298c53714 Mon Sep 17 00:00:00 2001 From: pschatzmann Date: Sat, 16 Sep 2023 20:11:05 +0200 Subject: [PATCH] HLS Draft --- src/AudioBasic/Collections/Vector.h | 4 + src/AudioCodecs/AudioEncoded.h | 18 +- src/AudioCodecs/CodecMTS.h | 103 ++++-- src/AudioHttp/HLSStream.h | 537 +++++++++++++++++++++------- src/AudioHttp/HttpHeader.h | 15 +- src/AudioHttp/HttpRequest.h | 10 +- src/AudioHttp/URLStream.h | 95 +++-- src/AudioHttp/Url.h | 8 +- src/AudioI2S/I2SConfig.h | 3 +- src/AudioI2S/I2SStream.h | 2 +- src/AudioLibs/AudioKit.h | 7 +- src/AudioTools/AudioLogger.h | 40 +++ src/AudioTools/AudioTypes.h | 13 +- 13 files changed, 625 insertions(+), 230 deletions(-) diff --git a/src/AudioBasic/Collections/Vector.h b/src/AudioBasic/Collections/Vector.h index 288545ab20..6925582ba4 100644 --- a/src/AudioBasic/Collections/Vector.h +++ b/src/AudioBasic/Collections/Vector.h @@ -303,6 +303,10 @@ class Vector { return p_data!=nullptr; } + void set_capacity(int size){ + resize_internal(size, false, false); + } + protected: int bufferLen=0; int len = 0; diff --git a/src/AudioCodecs/AudioEncoded.h b/src/AudioCodecs/AudioEncoded.h index 37b3a534cf..730f8fe503 100644 --- a/src/AudioCodecs/AudioEncoded.h +++ b/src/AudioCodecs/AudioEncoded.h @@ -326,6 +326,7 @@ class EncodedAudioOutput : public AudioStream { /// Starts the processing - sets the status to active bool begin() override { + custom_log_level.set(); TRACED(); if (!active) { TRACED(); @@ -338,6 +339,7 @@ class EncodedAudioOutput : public AudioStream { LOGW("no decoder or encoder defined"); } } + custom_log_level.reset(); return active; } @@ -349,14 +351,17 @@ class EncodedAudioOutput : public AudioStream { /// Ends the processing void end() override { + custom_log_level.set(); TRACEI(); decoder_ptr->end(); encoder_ptr->end(); active = false; + custom_log_level.reset(); } /// encodeor decode the data virtual size_t write(const uint8_t *data, size_t len) override { + custom_log_level.set(); LOGD("EncodedAudioOutput::write: %d", (int)len); if (len == 0) { LOGI("write: %d", 0); @@ -370,6 +375,8 @@ class EncodedAudioOutput : public AudioStream { size_t result = writer_ptr->write(data, len); LOGD("EncodedAudioOutput::write: %d -> %d", (int)len, (int)result); + custom_log_level.reset(); + return result; } @@ -384,6 +391,11 @@ class EncodedAudioOutput : public AudioStream { /// Provides the initialized encoder AudioEncoder &encoder() { return *encoder_ptr; } + /// Defines the class specific custom log level + void setLogLevel(AudioLogger::LogLevel level){ + custom_log_level.set(level); + } + protected: AudioInfo info; AudioDecoder *decoder_ptr = CodecNOP::instance(); // decoder @@ -391,6 +403,7 @@ class EncodedAudioOutput : public AudioStream { AudioWriter *writer_ptr = nullptr; Print *ptr_out = nullptr; bool active; + CustomLogLevel custom_log_level; }; // legacy name @@ -470,13 +483,16 @@ class EncodedAudioStream : public EncodedAudioOutput { } size_t readBytes(uint8_t *buffer, size_t length) override { + custom_log_level.set(); LOGD("EncodedAudioStream::readBytes: %d", (int)length); if (p_stream == nullptr) { TRACEE(); return 0; } decode(reqested_bytes); - return decoded_buffer.readArray(buffer, length); + size_t result = decoded_buffer.readArray(buffer, length); + custom_log_level.reset(); + return result; } protected: diff --git a/src/AudioCodecs/CodecMTS.h b/src/AudioCodecs/CodecMTS.h index 5b0b30d3f8..c9e3f52942 100644 --- a/src/AudioCodecs/CodecMTS.h +++ b/src/AudioCodecs/CodecMTS.h @@ -114,6 +114,13 @@ class MTSDecoder : public AudioDecoder { stream_types.push_back(type); } + bool isStreamTypeActive(TSDPMTStreamType type){ + for (int j=0;j buffer{MTS_WRITE_BUFFER_SIZE}; Vector stream_types; + void set_write_active(bool flag){ + //LOGD("is_write_active: %s", flag ? "true":"false"); + is_write_active = flag; + } + void demux(int limit){ TRACED(); TSDCode res = TSD_OK; @@ -152,7 +165,7 @@ class MTSDecoder : public AudioDecoder { void logResult(TSDCode code) { switch (code) { case TSD_OK: - LOGI("TSD_OK"); + LOGD("TSD_OK"); break; case TSD_INVALID_SYNC_BYTE: LOGW("TSD_INVALID_SYNC_BYTE"); @@ -202,7 +215,7 @@ class MTSDecoder : public AudioDecoder { // event callback static void event_cb(TSDemuxContext *ctx, uint16_t pid, TSDEventId event_id, void *data) { - TRACEI(); + TRACED(); if (MTSDecoder::self != nullptr) { MTSDecoder::self->event_cb_local(ctx, pid, event_id, data); } @@ -211,27 +224,26 @@ class MTSDecoder : public AudioDecoder { void event_cb_local(TSDemuxContext *ctx, uint16_t pid, TSDEventId event_id, void *data) { if (event_id == TSD_EVENT_PAT) { + set_write_active(false); print_pat(ctx, data); } else if (event_id == TSD_EVENT_PMT) { + set_write_active(false); print_pmt(ctx, data); } else if (event_id == TSD_EVENT_PES) { TSDPESPacket *pes = (TSDPESPacket *)data; - // This is where we would write the PES data into our buffer. - LOGI("===================="); - LOGI("PID %d PES Packet, Size: %ld, stream_id=%u, pts=%lu, dts=%lu", pid, + // This is where we write the PES data into our buffer. + LOGD("===================="); + LOGD("PID %d PES Packet, Size: %ld, stream_id=%u, pts=%lu, dts=%lu", pid, pes->data_bytes_length, pes->stream_id, pes->pts, pes->dts); // print out the PES Packet data if it's in our print list int i; AudioLogger logger = AudioLogger::instance(); for (i = 0; i < MTS_PRINT_PIDS_LEN; ++i) { if (print_pids[i] == pid) { - // output data - if (p_print != nullptr) { - p_print->write(pes->data_bytes, pes->data_bytes_length); - } // log data if (logger.isLogging(AudioLogger::Debug)) { - LOGI(" PES data: "); + logger.print(" PES data "); + logger.print(is_write_active? "active:":"inactive:"); int j = 0; while (j < pes->data_bytes_length) { char n = pes->data_bytes[j]; @@ -240,10 +252,19 @@ class MTSDecoder : public AudioDecoder { } logger.printChar('\n'); } + // output data + if (p_print != nullptr) { + size_t eff = p_print->write(pes->data_bytes, pes->data_bytes_length); + if(eff!=pes->data_bytes_length){ + // we should not get here + TRACEE(); + } + } } } } else if (event_id == TSD_EVENT_ADAP_FIELD_PRV_DATA) { + set_write_active(false); // we're only watching for SCTE Adaptions Field Private Data, // so we know that we must parse it as a list of descritors. TSDAdaptationField *adap_field = (TSDAdaptationField *)data; @@ -253,61 +274,65 @@ class MTSDecoder : public AudioDecoder { adap_field->transport_private_data_length, &descriptors, &descriptors_length); - LOGI("===================="); - LOGI("Descriptors - Adaptation Fields"); + LOGD("===================="); + LOGD("Descriptors - Adaptation Fields"); int i = 0; for (; i < descriptors_length; ++i) { TSDDescriptor *des = &descriptors[i]; - LOGI(" %d) tag: (0x%04X) %s", i, des->tag, + LOGD(" %d) tag: (0x%04X) %s", i, des->tag, descriptor_tag_to_str(des->tag)); - LOGI(" length: %d", des->length); + LOGD(" length: %d", des->length); print_descriptor_info(des); } } } void print_pat(TSDemuxContext *ctx, void *data) { - LOGI("===================="); + LOGD("===================="); TSDPATData *pat = (TSDPATData *)data; size_t len = pat->length; size_t i; - LOGI("PAT, Length %d", (int)pat->length); + LOGD("PAT, Length %d", (int)pat->length); if (len > 1) { - LOGI("number of progs: %d", (int)len); + LOGD("number of progs: %d", (int)len); } for (i = 0; i < len; ++i) { - LOGI(" %d) prog num: 0x%X, pid: 0x%X", (int)i, pat->program_number[i], + LOGD(" %d) prog num: 0x%X, pid: 0x%X", (int)i, pat->program_number[i], pat->pid[i]); } } void print_pmt(TSDemuxContext *ctx, void *data) { - LOGI("===================="); - LOGI("PMT"); + LOGD("===================="); + LOGD("PMT"); TSDPMTData *pmt = (TSDPMTData *)data; - LOGI("PCR PID: 0x%04X", pmt->pcr_pid); - LOGI("program info length: %d", (int)pmt->program_info_length); - LOGI("descriptors length: %d", (int)pmt->descriptors_length); + LOGD("PCR PID: 0x%04X", pmt->pcr_pid); + LOGD("program info length: %d", (int)pmt->program_info_length); + LOGD("descriptors length: %d", (int)pmt->descriptors_length); size_t i; for (i = 0; i < pmt->descriptors_length; ++i) { TSDDescriptor *des = &pmt->descriptors[i]; - LOGI(" %d) tag: (0x%04X) %s", (int)i, des->tag, + LOGD(" %d) tag: (0x%04X) %s", (int)i, des->tag, descriptor_tag_to_str(des->tag)); - LOGI(" length: %d", des->length); + LOGD(" length: %d", des->length); print_descriptor_info(des); } - LOGI("program elements length: %d", (int)pmt->program_elements_length); + LOGD("program elements length: %d", (int)pmt->program_elements_length); for (i = 0; i < pmt->program_elements_length; ++i) { TSDProgramElement *prog = &pmt->program_elements[i]; - LOGI(" -----Program #%d", (int)i); - LOGI(" stream type: (0x%04X) %s", prog->stream_type, + LOGD(" -----Program #%d", (int)i); + LOGD(" stream type: (0x%04X) %s", prog->stream_type, stream_type_to_str((TSDPESStreamId)(prog->stream_type))); - LOGI(" elementary pid: 0x%04X", prog->elementary_pid); - LOGI(" es info length: %d", prog->es_info_length); - LOGI(" descriptors length: %d", (int)prog->descriptors_length); + LOGD(" elementary pid: 0x%04X", prog->elementary_pid); + LOGD(" es info length: %d", prog->es_info_length); + LOGD(" descriptors length: %d", (int)prog->descriptors_length); + + if (isStreamTypeActive((TSDPMTStreamType)prog->stream_type)){ + set_write_active(true); + } // keep track of metadata pids, we'll print the data for these for(int j=0;jdescriptors_length; ++j) { TSDDescriptor *des = &prog->descriptors[j]; - LOGI(" %d) tag: (0x%04X) %s", (int)j, des->tag, + LOGD(" %d) tag: (0x%04X) %s", (int)j, des->tag, descriptor_tag_to_str(des->tag)); - LOGI(" length: %d", des->length); + LOGD(" length: %d", des->length); print_descriptor_info(des); // if this tag is the SCTE Adaption field private data descriptor, @@ -784,7 +809,7 @@ class MTSDecoder : public AudioDecoder { TSDDescriptorRegistration res; if (TSD_OK == tsd_parse_descriptor_registration( desc->data, desc->data_length, &res)) { - LOGI("\n format identififer: 0x%08X", res.format_identifier); + LOGD("\n format identififer: 0x%08X", res.format_identifier); } } break; case 0x0A: // ISO 639 Language descriptor @@ -792,13 +817,13 @@ class MTSDecoder : public AudioDecoder { TSDDescriptorISO639Language res; if (TSD_OK == tsd_parse_descriptor_iso639_language( desc->data, desc->data_length, &res)) { - LOGI("\n"); + LOGD("\n"); int i = 0; for (; i < res.language_length; ++i) { - LOGI(" ISO Language Code: 0x%08X, audio type: 0x%02x", + LOGD(" ISO Language Code: 0x%08X, audio type: 0x%02x", res.iso_language_code[i], res.audio_type[i]); } - LOGI("\n"); + LOGD("\n"); } } break; case 0x0E: // Maximum bitrate descriptor @@ -806,7 +831,7 @@ class MTSDecoder : public AudioDecoder { TSDDescriptorMaxBitrate res; if (TSD_OK == tsd_parse_descriptor_max_bitrate( desc->data, desc->data_length, &res)) { - LOGI(" Maximum Bitrate: %d x 50 bytes/second", res.max_bitrate); + LOGD(" Maximum Bitrate: %d x 50 bytes/second", res.max_bitrate); } } break; default: { @@ -834,7 +859,7 @@ class MTSDecoder : public AudioDecoder { } static void log_free (void *mem){ - LOGI("free(%p)\n", mem); + LOGD("free(%p)\n", mem); free(mem); } diff --git a/src/AudioHttp/HLSStream.h b/src/AudioHttp/HLSStream.h index facfcf9f6f..0696e3442e 100644 --- a/src/AudioHttp/HLSStream.h +++ b/src/AudioHttp/HLSStream.h @@ -6,160 +6,421 @@ #include "AudioBasic/StrExt.h" #include "AudioHttp/URLStream.h" -#define MAX_HLS_LINE 200 +#define MAX_HLS_LINE 400 namespace audio_tools { -/** - * Simple Parser for HLS data. We select the entry with min bandwidth - */ -class HLSParser { - public: - bool begin(const char *urlStr) { - index_url_str = urlStr; - segments_url_str = ""; - bandwidth = 0; - LOGI("Loading index: %s", index_url_str); - url_stream.setTimeout(1000); - url_stream.setConnectionClose(false); - // we only update the content length - url_stream.httpRequest().reply().put(CONTENT_LENGTH, 0); - url_stream.setAutoCreateLines(false); - - bool rc = url_stream.begin(index_url_str); - if (rc) rc = parse(true); - return rc; +/*** + * @brief We feed the URLLoader with some url strings. At each readBytes or available() call + * we refill the buffer. The buffer must be big enough to bridge the delays caused by the + * reloading of the segments + * @author Phil Schatzmann + * @copyright GPLv3 +*/ + +class URLLoader { + public: + URLLoader() = default; + ~URLLoader(){ + end(); } bool begin() { - TRACEI(); - segments_url_str = ""; - bandwidth = 0; - LOGI("-------------------"); - LOGI("Loading index: %s", index_url_str); - bool rc = url_stream.begin(index_url_str); - if (rc) rc = parse(true); - return rc; + TRACED(); + active = true; + return true; } - // parse the index file and the segments - bool parse(bool process_index) { - LOGI("parsing %s", process_index ? "Index" : "Segements") - char tmp[MAX_HLS_LINE]; - bool result = true; - is_extm3u = false; + void end(){ + TRACED(); + stream.end(); + buffer.clear(); + active = false; + } - // parse lines - memset(tmp, 0, MAX_HLS_LINE); - while (url_stream.available()) { - memset(tmp, 0, MAX_HLS_LINE); - url_stream.httpRequest().readBytesUntil('\n', tmp, MAX_HLS_LINE); - Str str(tmp); + /// Adds the next url to be played in sequence + void addUrl(const char* url){ + LOGI("Adding %s", url); + Str url_str(url); + char *str = new char[url_str.length()+1]; + memcpy(str, url_str.c_str(), url_str.length()+1); + urls.push_back(str); + } - if (!str.startsWith("#EXTM3U")) { - is_extm3u = true; - } + /// Provides the number of open urls which can be played. Refills them, when min limit is reached. + int urlCount() { + return urls.size(); + } - if (process_index) { - parseIndex(str); - } else { - parseSegments(str); + /// Available bytes of the audio stream + int available() { + if(!active) return 0; + TRACED(); + bufferRefill(); + return buffer.available(); + } + + /// Provides data from the audio stream + size_t readBytes(uint8_t *data, size_t len) { + if(!active) return 0; + TRACED(); + bufferRefill(); + return buffer.readArray(data, len); + } + + const char *contentType() { + if (!stream) return nullptr; + return stream.httpRequest().reply().get(CONTENT_TYPE); + } + + const char *contentLength() { + if (!stream) return nullptr; + return stream.httpRequest().reply().get(CONTENT_LENGTH); + } + +protected: + Vector urls{10}; + NBuffer buffer{DEFAULT_BUFFER_SIZE, 50}; + bool active = false; + URLStream stream; + + + /// try to keep the buffer filled + void bufferRefill() { + TRACED(); + // we have nothing to do + if (urls.empty()) return; + if (buffer.availableForWrite()==0) return; + + // switch current stream if we have no more data + if ((!stream || stream.totalRead()==stream.contentLength()) && !urls.empty()) { + const char* url = urls[0]; + assert(stream.available()==0); + urls.pop_front(); + +#ifdef ESP32 + LOGI("Free heap: %d", ESP.getFreeHeap()); +#endif + LOGI("Playing %s of %d", url, urls.size()); + + stream.clear(); + if (!stream.begin(url)){ + TRACEE(); } + // free memory + delete(url); } - // load segments - if (process_index && !segments_url_str.isEmpty()) { - endUrlStream(); - LOGI("Load segments from: %s", segments_url_str.c_str()); - if (url_stream.begin(segments_url_str.c_str())) { - result = parse(false); + // copy data to buffer + int to_write = min(buffer.availableForWrite(),DEFAULT_BUFFER_SIZE); + if (to_write>0){ + int total = 0; + while(to_write>0){ + uint8_t tmp[to_write]={0}; + int read = stream.readBytes(tmp, to_write); + total += read; + if (read>0){ + if (stream.totalRead()==stream.contentLength()) break; + buffer.writeArray(tmp, read); + to_write = min(buffer.availableForWrite(),DEFAULT_BUFFER_SIZE); + } else { + delay(50); + } } + LOGD("Refilled with %d now %d available to write", total, buffer.availableForWrite()); } - return result; } +}; - Queue &getSegments() { return segments; } +/** + * Prevent that the same url is loaded twice. We limit the history to + * 20 entries. +*/ +class URLHistory { + public: + bool add(const char *url){ + bool found = false; + Str url_str(url); + for (int j=0;j20){ + delete(history[0]); + history.pop_front(); + } + } + return !found; + } - /// Provide access to the actual data stream - URLStream &getURLStream() { return url_stream; } + void clear(){ + history.clear(); + } + protected: + Vector history; +}; - /// Get the data from the next segment - bool nextStream() { +/** + * @brief Simple Parser for HLS data. We select the entry with min bandwidth + * @author Phil Schatzmann + * @copyright GPLv3 + */ +class HLSParser { + public: + // loads the index url + bool begin(const char *urlStr) { + index_url_str = urlStr; + return begin(); + } + + bool begin() { TRACEI(); - bool result = false; - StrExt url1; - URLStream &url_stream = getURLStream(); - if (getSegments().dequeue(url1)) { - endUrlStream(); - StrExt tmp; - // if the segment is a complete http url we use it - if (url1.startsWith("http")) { - tmp.set(url1.c_str()); - } else { - // we create the complete url - tmp.set(segments_url_str.c_str()); - tmp.add("/"); - tmp.add(url1.c_str()); - } - LOGI("-------------------"); - LOGI("playing %s", tmp.c_str()); - endUrlStream(); - result = url_stream.begin(tmp.c_str(), "audio/mp4a", GET); - } else { - LOGW("No more segments"); + custom_log_level.set(); + segments_url_str = ""; + bandwidth = 0; + if (!parseIndex()){ + TRACEE(); + return false; + } + if (!parseSegments()){ + TRACEE(); + return false; + } + if (!url_loader.begin()){ + TRACEE(); + return false; } + custom_log_level.reset(); + return true; + } + + int available() { + TRACED(); + if (!active) return 0; + custom_log_level.set(); + reloadSegments(this); + int result = url_loader.available(); + custom_log_level.reset(); return result; } + size_t readBytes(uint8_t* buffer, size_t len){ + TRACED(); + if (!active) return 0; + custom_log_level.set(); + reloadSegments(this); + size_t result = url_loader.readBytes(buffer, len); + custom_log_level.reset(); + return result; + } + + const char *indexUrl() { + return index_url_str; + } + + const char *segmentsUrl() { + if (segments_url_str==nullptr) return nullptr; + return segments_url_str.c_str(); + } + /// Provides the codec const char *getCodec() { return codec.c_str(); } + /// Provides the content type of the audio data const char *contentType() { - return url_stream.httpRequest().reply().get(CONTENT_TYPE); + return url_loader.contentType(); } const char *contentLength() { - return url_stream.httpRequest().reply().get(CONTENT_LENGTH); + return url_loader.contentLength(); } /// Closes the processing void end() { - TRACED(); - segments.clear(); + TRACEI(); + //timer.end(); codec.clear(); segments_url_str.clear(); - endUrlStream(); + url_stream.end(); + url_loader.end(); + url_history.clear(); + } + + /// Defines the number of urls that are preloaded in the URLLoader + void setUrlCount(int count){ + url_count = count; + } + + /// Defines the class specific custom log level + void setLogLevel(AudioLogger::LogLevel level){ + custom_log_level.set(level); } protected: + CustomLogLevel custom_log_level; int bandwidth = 0; + int url_count = 5; bool url_active = false; bool is_extm3u = false; StrExt codec; StrExt segments_url_str; + StrExt url_str; const char *index_url_str = nullptr; - Queue segments; URLStream url_stream; + URLLoader url_loader; + bool active = false; + int media_sequence = 0; + int tartget_duration_ms=5000; + int segment_count; + uint64_t next_sement_load_time = 0; + URLHistory url_history; + + // trigger the reloading of segments if the limit is underflowing + static void reloadSegments(void *ref){ + TRACED(); + HLSParser *self = (HLSParser*)ref; + // get new urls + if (!self->segments_url_str.isEmpty() + && self->tartget_duration_ms!=0){ + //&& millis() > self->next_sement_load_time){ + //self->next_sement_load_time = millis() + self->tartget_duration_ms; + self->parseSegments(); + } + } - void endUrlStream() { + // parse the index file and the segments + bool parseIndex() { TRACED(); - url_stream.end(); + url_stream.setTimeout(1000); + url_stream.setConnectionClose(false); + // we only update the content length + url_stream.httpRequest().reply().put(CONTENT_LENGTH, 0); + url_stream.setAutoCreateLines(false); + bool rc = url_stream.begin(index_url_str); + url_active = true; + rc = parse(true); + return rc; + } + + // parse the segment url provided by the index + bool parseSegments() { + TRACED(); + if (millis()=0) { + is_extm3u = true; + } + + if (is_extm3u) { + if (process_index) { + if (!parseIndexLine(str)){ + return false; + } + } else { + if (!parseSegmentLine(str)){ + return false; + } + } + } + } + + return result; } // Add all segments to queue - void parseSegments(Str str) { + bool parseSegmentLine(Str &str) { TRACED(); LOGI("> %s", str.c_str()); - if (!str.startsWith("#")) { + int pos = str.indexOf("#"); + if (pos>=0){ LOGI("-> Segment: %s", str.c_str()); - StrExt ts = str; - segments.enqueue(ts); + + pos = str.indexOf("#EXT-X-MEDIA-SEQUENCE:"); + if (pos>=0){ + int new_media_sequence = atoi(str.c_str()+pos+22); + LOGI("media_sequence: %d", new_media_sequence); + if (new_media_sequence == media_sequence){ + LOGW("MEDIA-SEQUENCE already loaded: %d", media_sequence); + return false; + } + media_sequence = new_media_sequence; + } + + pos = str.indexOf("#EXT-X-TARGETDURATION:"); + if (pos>=0){ + const char* duration_str = str.c_str()+pos+22; + tartget_duration_ms = 1000 * atoi(duration_str); + LOGI("tartget_duration_ms: %s %d",duration_str, tartget_duration_ms); + // use updated value + } + } else { + segment_count++; + if (url_history.add(str.c_str())){ + // provide audio urls to the url_loader + if (str.startsWith("http")) { + url_str = str; + } else { + // we create the complete url + url_str = segments_url_str; + url_str.add("/"); + url_str.add(str.c_str()); + } + url_loader.addUrl(url_str.c_str()); + } else { + LOGD("Douplicate ignored: %s", str.c_str()); + } } + return true; } - // Determine codec for min badnwidth - void parseIndex(Str str) { + // Determine codec for min bandwidth + bool parseIndexLine(Str &str) { TRACED(); LOGI("> %s", str.c_str()); int tmp_bandwidth; @@ -185,18 +446,19 @@ class HLSParser { } } - if (url_active && str.startsWith("http")) { - if (str.startsWith("http")) { - // check if we have a valid codec - segments_url_str.set(str); - } + if (str.startsWith("http")) { + // check if we have a valid codec + segments_url_str.set(str); + LOGD("segments_url_str = %s", str.c_str()); } + + return true; } }; /** - * @brief HTTP Live Streaming using HLS. The result is a MPEG-TS data stream - * that must be decuded with a DecoderMTS. + * @brief HTTP Live Streaming using HLS: The result is a MPEG-TS data stream + * that must be decoded e.g. with a DecoderMTS. * * @author Phil Schatzmann * @ingroup http *@copyright GPLv3 @@ -212,34 +474,28 @@ class HLSStream : public AudioStream { } bool begin(const char *urlStr) { + TRACEI(); + login(); // parse the url to the HLS bool rc = parser.begin(urlStr); - - // trigger first access to data - available(); return rc; } - bool begin() { return parser.begin(); } + bool begin() { + TRACEI(); + login(); + bool rc = parser.begin(); + return rc; + } // ends the request void end() { parser.end(); } - /// provides access to the HttpRequest - HttpRequest &httpRequest() { return parser.getURLStream().httpRequest(); } - - /// (Re-)defines the client - void setClient(Client &clientPar) { - parser.getURLStream().setClient(clientPar); - } - /// Sets the ssid that will be used for logging in (when calling begin) - void setSSID(const char *ssid) { parser.getURLStream().setSSID(ssid); } + void setSSID(const char *ssid) { this->ssid = ssid; } /// Sets the password that will be used for logging in (when calling begin) - void setPassword(const char *password) { - parser.getURLStream().setPassword(password); - } + void setPassword(const char *password) {this->password = password;} /// Returns the string representation of the codec of the audio stream const char *codec() { return parser.getCodec(); } @@ -252,35 +508,42 @@ class HLSStream : public AudioStream { return parser.contentLength(); } - int available() override { - Stream &urlStream = getURLStream(); - int result = urlStream.available(); - if (result == 0) { - if (!parser.nextStream()) { - // we consumed all segments so we get new ones - begin(); - } - result = urlStream.available(); - } - return result; + TRACED(); + return parser.available(); } size_t readBytes(uint8_t *data, size_t len) override { - Stream &urlStream = getURLStream(); - size_t result = 0; - if (urlStream.available() > 0) { - result = urlStream.readBytes(data, len); - } - return result; + TRACED(); + return parser.readBytes(data, len); + } + + /// Defines the class specific custom log level + void setLogLevel(AudioLogger::LogLevel level){ + parser.setLogLevel(level); } protected: HLSParser parser; - Print *p_out = nullptr; - AudioInfoSupport *p_ai = nullptr; + const char* ssid = nullptr; + const char* password = nullptr; + + void login(){ +#ifdef USE_WIFI + if (ssid!=nullptr && password != nullptr && WiFi.status() != WL_CONNECTED){ + TRACED(); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED){ + Serial.print("."); + delay(500); + } + } +#else + LOGW("login not supported"); +#endif + } + - Stream &getURLStream() { return parser.getURLStream(); } }; } // namespace audio_tools diff --git a/src/AudioHttp/HttpHeader.h b/src/AudioHttp/HttpHeader.h index 75335bfc44..b9cc07253c 100644 --- a/src/AudioHttp/HttpHeader.h +++ b/src/AudioHttp/HttpHeader.h @@ -60,14 +60,14 @@ struct HttpHeaderLine { class HttpHeader { public: HttpHeader(){ - LOGI("HttpHeader"); - // set default values + LOGD("HttpHeader"); + // set default values protocol_str = "HTTP/1.1"; url_path = "/"; status_msg = ""; } ~HttpHeader(){ - LOGI("~HttpHeader"); + LOGD("~HttpHeader"); clear(true); } @@ -233,10 +233,15 @@ class HttpHeader { char line[MAX_HTTP_HEADER_LINE_LENGTH]; if (in.connected()){ if (in.available()==0) { - LOGW("Waiting for data..."); + int count = 0; while(in.available()==0){ - delay(500); + delay(50); + count++; + if (count==2){ + LOGI("Waiting for data..."); + } } + LOGI("Data availble"); } readLine(in, line, MAX_HTTP_HEADER_LINE_LENGTH); parse1stLine(line); diff --git a/src/AudioHttp/HttpRequest.h b/src/AudioHttp/HttpRequest.h index 81098374b6..e57aad7041 100644 --- a/src/AudioHttp/HttpRequest.h +++ b/src/AudioHttp/HttpRequest.h @@ -35,11 +35,11 @@ class HttpRequest { friend class URLStream; HttpRequest() { - LOGI("HttpRequest"); + LOGD("HttpRequest"); } HttpRequest(Client &client){ - LOGI("HttpRequest"); + LOGD("HttpRequest"); setClient(client); } @@ -171,7 +171,7 @@ class HttpRequest { this->accept = mime; } - size_t getReceivedContentLength() { + size_t contentLength() { const char *len_str = reply().get(CONTENT_LENGTH); int len = 0; if (len_str != nullptr) { @@ -304,6 +304,10 @@ class HttpRequest { http_connect_callback = callback; } + void setTimeout(int timeout){ + clientTimeout = timeout; + } + protected: Client *client_ptr; Url url; diff --git a/src/AudioHttp/URLStream.h b/src/AudioHttp/URLStream.h index c2cb817327..808035787a 100644 --- a/src/AudioHttp/URLStream.h +++ b/src/AudioHttp/URLStream.h @@ -23,6 +23,7 @@ namespace audio_tools { + /** * @brief Represents the content of a URL as Stream. We use the WiFi.h API * @author Phil Schatzmann @@ -34,25 +35,25 @@ class URLStream : public AbstractURLStream { public: URLStream(int readBufferSize=DEFAULT_BUFFER_SIZE){ - TRACEI(); + TRACED(); setReadBufferSize(readBufferSize); } URLStream(Client &clientPar, int readBufferSize=DEFAULT_BUFFER_SIZE){ - TRACEI(); + TRACED(); setReadBufferSize(readBufferSize); setClient(clientPar); } URLStream(const char* network, const char *password, int readBufferSize=DEFAULT_BUFFER_SIZE) { - TRACEI(); + TRACED(); setReadBufferSize(readBufferSize); setSSID(network); setPassword(password); } ~URLStream(){ - TRACEI(); + TRACED(); end(); #ifdef USE_WIFI_CLIENT_SECURE if (clientSecure!=nullptr){ @@ -87,10 +88,14 @@ class URLStream : public AbstractURLStream { virtual bool begin(const char* urlStr, const char* acceptMime=nullptr, MethodID action=GET, const char* reqMime="", const char*reqData="") override{ LOGI( "%s: %s",LOG_METHOD, urlStr); - - url.setUrl(urlStr); + custom_log_level.set(); + url_str = urlStr; + url.setUrl(url_str.c_str()); int result = -1; + // close it - if we have an active connection + if (active) end(); + // optional: login if necessary login(); @@ -106,23 +111,28 @@ class URLStream : public AbstractURLStream { } result = process(action, url, reqMime, reqData); if (result>0){ - size = request.getReceivedContentLength(); + size = request.contentLength(); LOGI("size: %d", (int)size); - if (size>=0){ + if (size>=0 && wait_for_data){ waitForData(); } } + total_read = 0; active = result == 200; + custom_log_level.reset(); + return active; } virtual void end() override { active = false; request.stop(); + clear(); } virtual int available() override { if (!active || !request) return 0; + int result = request.available(); LOGD("available: %d", result); return result; @@ -204,6 +214,8 @@ class URLStream : public AbstractURLStream { httpRequest().reply().clear(false); httpRequest().header().clear(false); read_buffer.resize(0); + read_pos = 0; + read_size = 0; } /// Adds/Updates a request header @@ -216,8 +228,45 @@ class URLStream : public AbstractURLStream { request.setOnConnectCallback(callback); } + void setWaitForData(bool flag){ + wait_for_data = flag; + } + + size_t contentLength() { + return request.contentLength(); + } + + size_t totalRead() { + return total_read; + } + + /// waits for some data - returns false if the request has failed + virtual bool waitForData() { + TRACEI(); + if(request.available()==0 ){ + LOGI("Request written ... waiting for reply") + while(request.available()==0){ + // stop waiting if we got an error + if (request.reply().statusCode()>=300){ + LOGE("Error code recieved ... stop waiting for reply"); + break; + } + delay(500); + } + } + LOGI("available: %d", request.available()); + return request.available()>0; + } + + /// Defines the class specific custom log level + void setLogLevel(AudioLogger::LogLevel level){ + custom_log_level.set(level); + } + protected: HttpRequest request; + CustomLogLevel custom_log_level; + StrExt url_str; Url url; long size; long total_read; @@ -227,6 +276,7 @@ class URLStream : public AbstractURLStream { uint16_t read_pos; uint16_t read_size; bool active = false; + bool wait_for_data = true; // optional const char* network=nullptr; const char* password=nullptr; @@ -239,19 +289,18 @@ class URLStream : public AbstractURLStream { unsigned long handshakeTimeout = URL_HANDSHAKE_TIMEOUT; //120000 bool is_power_save = false; - void setTimeouts() { - // set regular timeout - getClient(url.isSecure()).setTimeout(clientTimeout/1000); // this is in seconds - } /// Process the Http request and handle redirects int process(MethodID action, Url &url, const char* reqMime, const char *reqData, int len=-1) { - request.setClient(getClient(url.isSecure())); + client = &getClient(url.isSecure()); + request.setClient(*client); // keep icy across redirect requests ? const char* icy = request.header().get("Icy-MetaData"); // set timeout - setTimeouts(); + client->setTimeout(clientTimeout / 1000); + request.setTimeout(clientTimeout); + #ifdef ESP32 // There is a bug in IDF 4! if (clientSecure!=nullptr){ @@ -312,6 +361,7 @@ class URLStream : public AbstractURLStream { #endif } + inline void fillBuffer() { if (isEOS()){ // if we consumed all bytes we refill the buffer @@ -341,23 +391,6 @@ class URLStream : public AbstractURLStream { #endif } - /// waits for some data - returns false if the request has failed - virtual bool waitForData() { - TRACEI(); - if(request.available()==0 ){ - LOGI("Request written ... waiting for reply") - while(request.available()==0){ - // stop waiting if we got an error - if (request.reply().statusCode()>=300){ - LOGE("Error code recieved ... stop waiting for reply"); - break; - } - delay(500); - } - } - LOGI("available: %d", request.available()); - return request.available()>0; - } }; } diff --git a/src/AudioHttp/Url.h b/src/AudioHttp/Url.h index d6879e57fb..2706b34c72 100644 --- a/src/AudioHttp/Url.h +++ b/src/AudioHttp/Url.h @@ -24,11 +24,11 @@ class Url { public: // empty url Url() { - LOGI("Url"); + LOGD("Url"); } ~Url() { - LOGI("~Url"); + LOGD("~Url"); pathStr.clear(); hostStr.clear(); protocolStr.clear(); @@ -38,13 +38,13 @@ class Url { // setup url with string Url(const char *url){ - LOGI("Url %s",url); + LOGD("Url %s",url); setUrl(url); } // copy constructor Url(Url &url){ - LOGI("Url %s",url.url()); + LOGD("Url %s",url.url()); setUrl(url.url()); } diff --git a/src/AudioI2S/I2SConfig.h b/src/AudioI2S/I2SConfig.h index 4d3931067f..4479df738b 100644 --- a/src/AudioI2S/I2SConfig.h +++ b/src/AudioI2S/I2SConfig.h @@ -119,7 +119,8 @@ class I2SConfig : public AudioInfo { #endif - void logInfo() { + void logInfo(const char* source=nullptr) { + AudioInfo::logInfo(source); LOGI("rx/tx mode: %s", RxTxModeNames[rx_tx_mode]); LOGI("port_no: %d", port_no); LOGI("is_master: %s", is_master ? "Master":"Slave"); diff --git a/src/AudioI2S/I2SStream.h b/src/AudioI2S/I2SStream.h index ed89272afe..48aff4abcf 100644 --- a/src/AudioI2S/I2SStream.h +++ b/src/AudioI2S/I2SStream.h @@ -81,7 +81,7 @@ class I2SStream : public AudioStream { cfg.sample_rate = info.sample_rate; cfg.bits_per_sample = info.bits_per_sample; cfg.channels = info.channels; - cfg.logInfo(); + cfg.logInfo("I2SStream"); i2s.end(); i2s.begin(cfg); diff --git a/src/AudioLibs/AudioKit.h b/src/AudioLibs/AudioKit.h index 712e83d6c5..f56c4b90d3 100644 --- a/src/AudioLibs/AudioKit.h +++ b/src/AudioLibs/AudioKit.h @@ -204,7 +204,7 @@ class AudioKitStream : public AudioStream { cfg = config; AudioStream::setAudioInfo(config); - cfg.logInfo(); + cfg.logInfo("AudioKitStream"); // start codec auto kit_cfg = cfg.toAudioKitConfig(); @@ -265,7 +265,6 @@ class AudioKitStream : public AudioStream { && is_started) { // update sample rate only cfg.sample_rate = info.sample_rate; - cfg.logInfo(); i2s_stream.setAudioInfo(cfg); kit.setSampleRate(cfg.toSampleRate()); } else if (cfg.sample_rate != info.sample_rate @@ -276,7 +275,7 @@ class AudioKitStream : public AudioStream { cfg.sample_rate = info.sample_rate; cfg.bits_per_sample = info.bits_per_sample; cfg.channels = info.channels; - cfg.logInfo(); + cfg.logInfo("AudioKit"); // Stop first if(is_started){ @@ -569,7 +568,7 @@ class AudioKitStream : public AudioStream { protected: AudioKit kit; I2SStream i2s_stream; - AudioKitStreamConfig cfg; + AudioKitStreamConfig cfg = defaultConfig(RXTX_MODE); AudioActions actions; int volume_value = 40; bool active = true; diff --git a/src/AudioTools/AudioLogger.h b/src/AudioTools/AudioLogger.h index 35adf1fcc8..fc40df8b29 100644 --- a/src/AudioTools/AudioLogger.h +++ b/src/AudioTools/AudioLogger.h @@ -15,6 +15,7 @@ namespace audio_tools { static portMUX_TYPE mutex_logger = portMUX_INITIALIZER_UNLOCKED; #endif + /** * @brief A simple Logger that writes messages dependent on the log level * @ingroup tools @@ -79,6 +80,10 @@ class AudioLogger { return log_level; } + void print(const char *c){ + log_print_ptr->print(c); + } + void printChar(char c){ log_print_ptr->print(c); } @@ -136,6 +141,41 @@ class AudioLogger { } }; +/// Class specific custom log level +class CustomLogLevel { +public: + AudioLogger::LogLevel getActual(){ + return actual; + } + + /// Defines a custom level + void set(AudioLogger::LogLevel level){ + active = true; + original = AudioLogger::instance().level(); + actual = level; + } + + /// sets the defined log level + void set(){ + if (active){ + AudioLogger::instance().begin(Serial, actual); + } + } + /// resets to the original log level + void reset(){ + if (active){ + AudioLogger::instance().begin(Serial, original); + } + } +protected: + bool active=false; + AudioLogger::LogLevel original; + AudioLogger::LogLevel actual; + +}; + + + } diff --git a/src/AudioTools/AudioTypes.h b/src/AudioTools/AudioTypes.h index f437c05819..91638c50f5 100644 --- a/src/AudioTools/AudioTypes.h +++ b/src/AudioTools/AudioTypes.h @@ -84,10 +84,15 @@ struct AudioInfo { return *this; } - virtual void logInfo() { - LOGI("sample_rate: %d", sample_rate); - LOGI("channels: %d", channels); - LOGI("bits_per_sample: %d", bits_per_sample); + virtual void logInfo(const char* source=nullptr) { + static AudioInfo old; + if (*this!=old){ + if(source!=nullptr) LOGI("Info from %s:", source); + LOGI("sample_rate: %d", sample_rate); + LOGI("channels: %d", channels); + LOGI("bits_per_sample: %d", bits_per_sample); + old = *this; + } } // public attributes