2#include "AudioTools/AudioCodecs/AudioEncoded.h"
3#include "AudioTools/CoreAudio/AudioBasic/Str.h"
4#include "AudioTools/Communication/HTTP/URLStream.h"
5#include "AudioTools/CoreAudio/StreamCopy.h"
6#include "AudioToolsConfig.h"
8#define MAX_HLS_LINE 512
9#define START_URLS_LIMIT 4
10#define HLS_BUFFER_COUNT 2
11#define HLS_MAX_NO_READ 2
12#define HLS_MAX_URL_LEN 256
13#define HLS_TIMEOUT 5000
14#define HLS_UNDER_OVERFLOW_WAIT_TIME 10
34template <
typename URLStream>
43 buffer.
resize(buffer_size * buffer_count);
58 LOGI(
"Adding %s", url);
60 char *str =
new char[url_str.length() + 1];
61 memcpy(str, url_str.c_str(), url_str.length() + 1);
62 urls.push_back((
const char *)str);
71 if (!active)
return 0;
80 if (!active)
return 0;
84 if (buffer.
available() < len) LOGW(
"Buffer underflow");
88 const char *contentType() {
89 return url_stream.httpRequest().reply().get(CONTENT_TYPE);
92 int contentLength() {
return url_stream.contentLength(); }
94 void setBufferSize(
int size,
int count) {
98 if (buffer.
size() != 0) {
99 buffer.
resize(buffer_size * buffer_count);
103 void setCACert(
const char *cert) { url_stream.setCACert(cert); }
106 Vector<const char *> urls{10};
107 RingBuffer<uint8_t> buffer{0};
109 int buffer_size = DEFAULT_BUFFER_SIZE;
110 int buffer_count = HLS_BUFFER_COUNT;
112 const char *url_to_play =
nullptr;
120 delay(HLS_UNDER_OVERFLOW_WAIT_TIME);
125 delay(HLS_UNDER_OVERFLOW_WAIT_TIME);
130 if (!url_stream && !urls.empty()) {
132 if (url_to_play !=
nullptr) {
135 url_to_play = urls[0];
136 LOGI(
"playing %s", url_to_play);
138 url_stream.setConnectionClose(
true);
139 url_stream.setTimeout(HLS_TIMEOUT);
140 url_stream.begin(url_to_play);
141 url_stream.waitForData(HLS_TIMEOUT);
145 LOGI(
"Playing %s of %d", url_stream.urlStr(), (
int)urls.size());
152 while (to_write > 0) {
153 uint8_t tmp[to_write];
154 memset(tmp, 0, to_write);
155 int read = url_stream.readBytes(tmp, to_write);
160 LOGD(
"buffer add %d -> %d:", read, buffer.
available());
167 if (url_stream.totalRead() == url_stream.contentLength()) {
168 LOGI(
"Closing stream because all bytes were processed: available: %d",
169 url_stream.available());
173 LOGD(
"Refilled with %d now %d available to write", total,
185 bool add(
const char *url) {
186 if (url ==
nullptr)
return true;
188 StrView url_str(url);
189 for (
int j = 0; j < history.size(); j++) {
190 if (url_str.equals(history[j])) {
196 char *str =
new char[url_str.length() + 1];
197 memcpy(str, url, url_str.length() + 1);
198 history.push_back((
const char *)str);
199 if (history.size() > 20) {
207 void clear() { history.clear(); }
209 int size() {
return history.size(); }
212 Vector<const char *> history;
220template <
typename URLStream>
224 bool begin(
const char *
urlStr) {
231 segments_url_str =
"";
241 if (url_loader.urlCount() == 0) {
247 segments_url_str = index_url_str;
251 if (!url_loader.begin()) {
264 if (active) result = url_loader.available();
268 size_t readBytes(uint8_t *data,
size_t len) {
273 if (active) result = url_loader.readBytes(data, len);
274 total_read += result;
278 const char *indexUrl() {
return index_url_str; }
280 const char *segmentsUrl() {
return segments_url_str.c_str(); }
295 segments_url_str.clear();
307 url_loader.setBufferSize(size, count);
310 void setCACert(
const char *cert) {
311 url_stream.setCACert(cert);
312 url_loader.setCACert(cert);
315 void setPowerSave(
bool flag) { url_stream.setPowerSave(flag); }
317 void setURLResolver(
const char *(*cb)(
const char *segment,
318 const char *reqURL)) {
322 const char *
urlStr() {
return url_str.c_str(); }
328 enum class URLType { Undefined, Index, Segment };
329 URLType next_url_type = URLType::Undefined;
332 size_t total_read = 0;
333 bool url_active =
false;
334 bool is_extm3u =
false;
336 Str segments_url_str;
338 const char *index_url_str =
nullptr;
339 URLStream url_stream;
340 URLLoaderHLS<URLStream> url_loader;
341 URLHistory url_history;
343 bool parse_segments_active =
false;
344 int media_sequence = 0;
345 int segment_count = 0;
346 uint64_t next_sement_load_time_planned = 0;
348 uint64_t next_sement_load_time = 0;
349 const char *(*resolve_url)(
const char *segment,
354 static const char *
resolveURL(
const char *segment,
const char *reqURL) {
356 static char result[HLS_MAX_URL_LEN] = {0};
357 StrView result_str(result, HLS_MAX_URL_LEN);
358 StrView index_url(reqURL);
360 int end = index_url.lastIndexOf(
"?");
362 result_str.substring(reqURL, 0,
end);
364 end = index_url.lastIndexOf(
"/");
366 result_str.substring(reqURL, 0,
end);
370 if (result_str.isEmpty()) {
374 if (!result_str.endsWith(
"/")) {
378 result_str.add(segment);
379 LOGI(
">> relative addr: %s for %s", segment, reqURL);
380 LOGD(
">> -> %s", result);
388 if (!segments_url_str.isEmpty()) {
397 url_stream.setTimeout(HLS_TIMEOUT);
398 url_stream.setConnectionClose(
true);
399 if (!url_stream.begin(index_url_str))
return false;
407 char tmp[MAX_HLS_LINE];
412 memset(tmp, 0, MAX_HLS_LINE);
414 memset(tmp, 0, MAX_HLS_LINE);
416 url_stream.httpRequest().readBytesUntil(
'\n', tmp, MAX_HLS_LINE);
418 if (len == 0 && url_stream.available() == 0)
break;
422 if (str.startsWith(
"#EXTM3U")) {
440 LOGI(
"> %s", str.c_str());
441 parseIndexLineMetaData(str);
443 parseSegmentLineMetaData(str);
448 bool parseIndexLineMetaData(StrView &str) {
450 if (str.startsWith(
"#")) {
451 if (str.indexOf(
"EXT-X-STREAM-INF") >= 0) {
452 next_url_type = URLType::Index;
454 int pos = str.indexOf(
"BANDWIDTH=");
456 StrView num(str.c_str() + pos + 10);
457 tmp_bandwidth = num.toInt();
458 url_active = (tmp_bandwidth < bandwidth || bandwidth == 0);
460 bandwidth = tmp_bandwidth;
461 LOGD(
"-> bandwith: %d", bandwidth);
465 pos = str.indexOf(
"CODECS=");
468 int end = str.indexOf(
'"', pos + 10);
469 codec.substring(str, start,
end);
470 LOGI(
"-> codec: %s", codec.c_str());
477 void resetTimings() {
478 next_sement_load_time_planned = millis();
480 next_sement_load_time = 0xFFFFFFFFFFFFFFFF;
486 if (parse_segments_active) {
491 if (millis() < next_sement_load_time && url_loader.urlCount() > 1) {
495 parse_segments_active =
true;
497 LOGI(
"Available urls: %d", url_loader.urlCount());
499 if (url_stream) url_stream.clear();
500 LOGI(
"parsing %s", segments_url_str.c_str());
502 if (segments_url_str.isEmpty()) {
504 parse_segments_active =
false;
508 if (!url_stream.begin(segments_url_str.c_str())) {
510 parse_segments_active =
false;
517 parse_segments_active =
false;
526 void segmentsActivate() {
527 LOGI(
"Reloading in %f sec", play_time / 1000.0);
529 next_sement_load_time = next_sement_load_time_planned + play_time;
533 if (url_history.size() > START_URLS_LIMIT) active =
true;
534 parse_segments_active =
false;
540 char tmp[MAX_HLS_LINE];
545 memset(tmp, 0, MAX_HLS_LINE);
547 memset(tmp, 0, MAX_HLS_LINE);
549 url_stream.httpRequest().readBytesUntil(
'\n', tmp, MAX_HLS_LINE);
550 if (len == 0 && url_stream.available() == 0)
break;
554 if (str.startsWith(
"#EXTM3U")) {
571 LOGI(
"> %s", str.c_str());
572 if (!parseSegmentLineMetaData(str))
return false;
577 bool parseSegmentLineMetaData(StrView &str) {
578 if (str.startsWith(
"#")) {
579 if (str.startsWith(
"#EXT-X-MEDIA-SEQUENCE:")) {
580 int new_media_sequence = atoi(str.c_str() + 22);
581 LOGI(
"media_sequence: %d", new_media_sequence);
582 if (new_media_sequence == media_sequence) {
583 LOGW(
"MEDIA-SEQUENCE already loaded: %d", media_sequence);
586 media_sequence = new_media_sequence;
590 if (str.startsWith(
"#EXTINF")) {
591 next_url_type = URLType::Segment;
592 StrView sec_str(str.c_str() + 8);
593 float sec = sec_str.toFloat();
594 LOGI(
"adding play time: %f sec", sec);
595 play_time += (sec * 1000.0);
601 bool parseLineURL(StrView &str) {
602 if (!str.startsWith(
"#")) {
603 switch (next_url_type) {
604 case URLType::Undefined:
609 if (str.startsWith(
"http")) {
610 segments_url_str.set(str);
612 segments_url_str.set(resolve_url(str.c_str(), index_url_str));
614 LOGD(
"segments_url_str = %s", segments_url_str.c_str());
616 case URLType::Segment:
618 if (url_history.add(str.c_str())) {
620 if (str.startsWith(
"http")) {
624 url_str = resolve_url(str.c_str(), index_url_str);
626 url_loader.addUrl(url_str.c_str());
628 LOGD(
"Duplicate ignored: %s", str.c_str());
632 next_url_type = URLType::Undefined;
652template <
typename URLStream>
669 bool rc = parser.begin(
urlStr);
677 bool rc = parser.begin();
682 void end()
override { parser.end(); }
685 void setSSID(
const char *ssid)
override { this->ssid = ssid; }
688 void setPassword(
const char *password)
override { this->password = password; }
691 const char *
codec() {
return parser.getCodec(); }
702 return parser.available();
708 return parser.readBytes(data, len);
712 void setBufferSize(
int size,
int count) { parser.setBufferSize(size, count); }
715 void setCACert(
const char *cert)
override { parser.setCACert(cert); }
723 const char *
codec = parser.getCodec();
724 const char *result =
nullptr;
725 if (
StrView(header).equalsIgnoreCase(CONTENT_TYPE)) {
726 result = parser.contentType();
728 if (result) LOGI(
"-> Format: %s", result);
735 const char *reqURL)) {
736 parser.setURLResolver(cb);
739 const char *
urlStr()
override {
return parser.urlStr(); }
741 size_t totalRead()
override {
return parser.totalRead(); };
749 const char *ssid =
nullptr;
750 const char *password =
nullptr;
754 if (ssid !=
nullptr && password !=
nullptr &&
755 WiFi.status() != WL_CONNECTED) {
758 WiFi.begin(ssid, password);
759 while (WiFi.status() != WL_CONNECTED) {
765 LOGW(
"login not supported");
770 bool begin(
const char *
urlStr,
const char *acceptMime, MethodID action = GET,
771 const char *reqMime =
"",
const char *reqData =
"")
override {
787using HLSStream = HLSStreamT<URLStream>;
Definition HLSStream.h:183