2#include "AudioTools/AudioCodecs/AudioEncoded.h"
3#include "AudioTools/CoreAudio/AudioBasic/Str.h"
4#include "AudioTools/CoreAudio/AudioHttp/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
27template <
typename URLStream>
36 buffer.resize(buffer_size * buffer_count);
51 LOGI(
"Adding %s", url);
53 char *str =
new char[url_str.length() + 1];
54 memcpy(str, url_str.c_str(), url_str.length() + 1);
55 urls.push_back((
const char *)str);
64 if (!active)
return 0;
73 if (!active)
return 0;
77 if (buffer.
available() < len) LOGW(
"Buffer underflow");
81 const char *contentType() {
82 return url_stream.httpRequest().reply().get(CONTENT_TYPE);
85 int contentLength() {
return url_stream.contentLength(); }
87 void setBufferSize(
int size,
int count) {
91 if (buffer.
size() != 0) {
92 buffer.resize(buffer_size * buffer_count);
96 void setCACert(
const char *cert) { url_stream.setCACert(cert); }
99 Vector<const char *> urls{10};
100 RingBuffer<uint8_t> buffer{0};
102 int buffer_size = DEFAULT_BUFFER_SIZE;
103 int buffer_count = HLS_BUFFER_COUNT;
105 const char *url_to_play =
nullptr;
113 delay(HLS_UNDER_OVERFLOW_WAIT_TIME);
118 delay(HLS_UNDER_OVERFLOW_WAIT_TIME);
123 if (!url_stream && !urls.empty()) {
125 if (url_to_play !=
nullptr) {
128 url_to_play = urls[0];
129 LOGI(
"playing %s", url_to_play);
131 url_stream.setConnectionClose(
true);
132 url_stream.setTimeout(HLS_TIMEOUT);
133 url_stream.begin(url_to_play);
134 url_stream.waitForData(HLS_TIMEOUT);
138 LOGI(
"Playing %s of %d", url_stream.urlStr(), (
int)urls.size());
145 while (to_write > 0) {
146 uint8_t tmp[to_write];
147 memset(tmp, 0, to_write);
148 int read = url_stream.readBytes(tmp, to_write);
153 LOGD(
"buffer add %d -> %d:", read, buffer.
available());
160 if (url_stream.totalRead() == url_stream.contentLength()) {
161 LOGI(
"Closing stream because all bytes were processed: available: %d",
162 url_stream.available());
166 LOGD(
"Refilled with %d now %d available to write", total,
178 bool add(
const char *url) {
179 if (url ==
nullptr)
return true;
181 StrView url_str(url);
182 for (
int j = 0; j < history.size(); j++) {
183 if (url_str.equals(history[j])) {
189 char *str =
new char[url_str.length() + 1];
190 memcpy(str, url, url_str.length() + 1);
191 history.push_back((
const char *)str);
192 if (history.size() > 20) {
200 void clear() { history.clear(); }
202 int size() {
return history.size(); }
205 Vector<const char *> history;
213template <
typename URLStream>
217 bool begin(
const char *
urlStr) {
224 segments_url_str =
"";
234 if (url_loader.urlCount() == 0) {
240 segments_url_str = index_url_str;
244 if (!url_loader.begin()) {
257 if (active) result = url_loader.available();
261 size_t readBytes(uint8_t *data,
size_t len) {
266 if (active) result = url_loader.readBytes(data, len);
267 total_read += result;
271 const char *indexUrl() {
return index_url_str; }
273 const char *segmentsUrl() {
return segments_url_str.c_str(); }
288 segments_url_str.clear();
300 url_loader.setBufferSize(size, count);
303 void setCACert(
const char *cert) {
304 url_stream.setCACert(cert);
305 url_loader.setCACert(cert);
308 void setPowerSave(
bool flag) { url_stream.setPowerSave(flag); }
310 void setURLResolver(
const char *(*cb)(
const char *segment,
311 const char *reqURL)) {
315 const char *
urlStr() {
return url_str.c_str(); }
321 enum class URLType { Undefined, Index, Segment };
322 URLType next_url_type = URLType::Undefined;
325 size_t total_read = 0;
326 bool url_active =
false;
327 bool is_extm3u =
false;
329 Str segments_url_str;
331 const char *index_url_str =
nullptr;
332 URLStream url_stream;
333 URLLoaderHLS<URLStream> url_loader;
334 URLHistory url_history;
336 bool parse_segments_active =
false;
337 int media_sequence = 0;
338 int segment_count = 0;
339 uint64_t next_sement_load_time_planned = 0;
341 uint64_t next_sement_load_time = 0;
342 const char *(*resolve_url)(
const char *segment,
347 static const char *
resolveURL(
const char *segment,
const char *reqURL) {
349 static char result[HLS_MAX_URL_LEN] = {0};
350 StrView result_str(result, HLS_MAX_URL_LEN);
351 StrView index_url(reqURL);
353 int end = index_url.lastIndexOf(
"?");
355 result_str.substring(reqURL, 0,
end);
357 end = index_url.lastIndexOf(
"/");
359 result_str.substring(reqURL, 0,
end);
363 if (result_str.isEmpty()) {
367 if (!result_str.endsWith(
"/")) {
371 result_str.add(segment);
372 LOGI(
">> relative addr: %s for %s", segment, reqURL);
373 LOGD(
">> -> %s", result);
347 static const char *
resolveURL(
const char *segment,
const char *reqURL) {
…}
381 if (!segments_url_str.isEmpty()) {
390 url_stream.setTimeout(HLS_TIMEOUT);
391 url_stream.setConnectionClose(
true);
392 if (!url_stream.begin(index_url_str))
return false;
400 char tmp[MAX_HLS_LINE];
405 memset(tmp, 0, MAX_HLS_LINE);
407 memset(tmp, 0, MAX_HLS_LINE);
409 url_stream.httpRequest().readBytesUntil(
'\n', tmp, MAX_HLS_LINE);
411 if (len == 0 && url_stream.available() == 0)
break;
415 if (str.startsWith(
"#EXTM3U")) {
433 LOGI(
"> %s", str.c_str());
434 parseIndexLineMetaData(str);
436 parseSegmentLineMetaData(str);
441 bool parseIndexLineMetaData(StrView &str) {
443 if (str.startsWith(
"#")) {
444 if (str.indexOf(
"EXT-X-STREAM-INF") >= 0) {
445 next_url_type = URLType::Index;
447 int pos = str.indexOf(
"BANDWIDTH=");
449 StrView num(str.c_str() + pos + 10);
450 tmp_bandwidth = num.toInt();
451 url_active = (tmp_bandwidth < bandwidth || bandwidth == 0);
453 bandwidth = tmp_bandwidth;
454 LOGD(
"-> bandwith: %d", bandwidth);
458 pos = str.indexOf(
"CODECS=");
461 int end = str.indexOf(
'"', pos + 10);
462 codec.substring(str, start,
end);
463 LOGI(
"-> codec: %s", codec.c_str());
470 void resetTimings() {
471 next_sement_load_time_planned = millis();
473 next_sement_load_time = 0xFFFFFFFFFFFFFFFF;
479 if (parse_segments_active) {
484 if (millis() < next_sement_load_time && url_loader.urlCount() > 1) {
488 parse_segments_active =
true;
490 LOGI(
"Available urls: %d", url_loader.urlCount());
492 if (url_stream) url_stream.clear();
493 LOGI(
"parsing %s", segments_url_str.c_str());
495 if (segments_url_str.isEmpty()) {
497 parse_segments_active =
false;
501 if (!url_stream.begin(segments_url_str.c_str())) {
503 parse_segments_active =
false;
510 parse_segments_active =
false;
519 void segmentsActivate() {
520 LOGI(
"Reloading in %f sec", play_time / 1000.0);
522 next_sement_load_time = next_sement_load_time_planned + play_time;
526 if (url_history.size() > START_URLS_LIMIT) active =
true;
527 parse_segments_active =
false;
533 char tmp[MAX_HLS_LINE];
538 memset(tmp, 0, MAX_HLS_LINE);
540 memset(tmp, 0, MAX_HLS_LINE);
542 url_stream.httpRequest().readBytesUntil(
'\n', tmp, MAX_HLS_LINE);
543 if (len == 0 && url_stream.available() == 0)
break;
547 if (str.startsWith(
"#EXTM3U")) {
564 LOGI(
"> %s", str.c_str());
565 if (!parseSegmentLineMetaData(str))
return false;
570 bool parseSegmentLineMetaData(StrView &str) {
571 if (str.startsWith(
"#")) {
572 if (str.startsWith(
"#EXT-X-MEDIA-SEQUENCE:")) {
573 int new_media_sequence = atoi(str.c_str() + 22);
574 LOGI(
"media_sequence: %d", new_media_sequence);
575 if (new_media_sequence == media_sequence) {
576 LOGW(
"MEDIA-SEQUENCE already loaded: %d", media_sequence);
579 media_sequence = new_media_sequence;
583 if (str.startsWith(
"#EXTINF")) {
584 next_url_type = URLType::Segment;
585 StrView sec_str(str.c_str() + 8);
586 float sec = sec_str.toFloat();
587 LOGI(
"adding play time: %f sec", sec);
588 play_time += (sec * 1000.0);
594 bool parseLineURL(StrView &str) {
595 if (!str.startsWith(
"#")) {
596 switch (next_url_type) {
597 case URLType::Undefined:
602 if (str.startsWith(
"http")) {
603 segments_url_str.set(str);
605 segments_url_str.set(resolve_url(str.c_str(), index_url_str));
607 LOGD(
"segments_url_str = %s", segments_url_str.c_str());
609 case URLType::Segment:
611 if (url_history.add(str.c_str())) {
613 if (str.startsWith(
"http")) {
617 url_str = resolve_url(str.c_str(), index_url_str);
619 url_loader.addUrl(url_str.c_str());
621 LOGD(
"Duplicate ignored: %s", str.c_str());
625 next_url_type = URLType::Undefined;
644template <
typename URLStream>
661 bool rc = parser.begin(
urlStr);
669 bool rc = parser.begin();
674 void end()
override { parser.end(); }
677 void setSSID(
const char *ssid)
override { this->ssid = ssid; }
680 void setPassword(
const char *password)
override { this->password = password; }
683 const char *
codec() {
return parser.getCodec(); }
694 return parser.available();
700 return parser.readBytes(data, len);
704 void setBufferSize(
int size,
int count) { parser.setBufferSize(size, count); }
707 void setCACert(
const char *cert)
override { parser.setCACert(cert); }
715 const char *
codec = parser.getCodec();
716 const char *result =
nullptr;
717 if (
StrView(header).equalsIgnoreCase(CONTENT_TYPE)) {
718 result = parser.contentType();
720 if (result) LOGI(
"-> Format: %s", result);
727 const char *reqURL)) {
728 parser.setURLResolver(cb);
731 const char *
urlStr()
override {
return parser.urlStr(); }
733 size_t totalRead()
override {
return parser.totalRead(); };
741 const char *ssid =
nullptr;
742 const char *password =
nullptr;
746 if (ssid !=
nullptr && password !=
nullptr &&
747 WiFi.status() != WL_CONNECTED) {
750 WiFi.begin(ssid, password);
751 while (WiFi.status() != WL_CONNECTED) {
757 LOGW(
"login not supported");
762 bool begin(
const char *
urlStr,
const char *acceptMime, MethodID action = GET,
763 const char *reqMime =
"",
const char *reqData =
"")
override {
762 bool begin(
const char *
urlStr,
const char *acceptMime, MethodID action = GET, {
…}
779using HLSStream = HLSStreamT<URLStream>;
Definition HLSStream.h:176