-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
shoutconnection.cpp
1096 lines (948 loc) · 37.5 KB
/
shoutconnection.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "engine/sidechain/shoutconnection.h"
#include <QRegularExpression>
#include <QTextCodec>
#include <QUrl>
// These includes are only required by ignoreSigpipe, which is unix-only
#ifndef __WINDOWS__
#include <signal.h>
#include <unistd.h>
#endif
// shout.h checks for WIN32 to see if we are on Windows.
#ifdef WIN64
#define WIN32
#endif
#include <shoutidjc/shout.h>
#ifdef WIN64
#undef WIN32
#endif
#include "broadcast/defs_broadcast.h"
#include "encoder/encoder.h"
#include "encoder/encoderbroadcastsettings.h"
#ifdef __OPUS__
#include "encoder/encoderopus.h"
#endif
#include "errordialoghandler.h"
#include "mixer/playerinfo.h"
#include "moc_shoutconnection.cpp"
#include "preferences/usersettings.h"
#include "recording/defs_recording.h"
#include "track/track.h"
#include "util/compatibility/qatomic.h"
#include "util/logger.h"
namespace {
constexpr int kConnectRetries = 30;
constexpr int kMaxNetworkCache = 491520; // 10 s mp3 @ 192 kbit/s
// Shoutcast default receive buffer 1048576 and autodumpsourcetime 30 s
// http://wiki.shoutcast.com/wiki/SHOUTcast_DNAS_Server_2
constexpr int kMaxShoutFailures = 3;
const QRegularExpression kArtistOrTitleRegex(QStringLiteral("\\$artist|\\$title"));
const QRegularExpression kArtistRegex(QStringLiteral("\\$artist"));
const mixxx::Logger kLogger("ShoutConnection");
} // namespace
ShoutConnection::ShoutConnection(BroadcastProfilePtr profile,
UserSettingsPointer pConfig)
: m_pTextCodec(nullptr),
m_pMetaData(),
m_pShout(nullptr),
m_pShoutMetaData(nullptr),
m_iMetaDataLife(0),
m_iShoutStatus(0),
m_iShoutFailures(0),
m_pConfig(pConfig),
m_pProfile(profile),
m_encoder(nullptr),
m_mainSamplerate(QStringLiteral("[App]"), QStringLiteral("samplerate")),
m_broadcastEnabled(BROADCAST_PREF_KEY, "enabled"),
m_custom_metadata(false),
m_firstCall(false),
m_format_is_mp3(false),
m_format_is_ov(false),
m_format_is_opus(false),
m_format_is_aac(false),
m_protocol_is_icecast1(false),
m_protocol_is_icecast2(false),
m_protocol_is_shoutcast(false),
m_ogg_dynamic_update(false),
m_threadWaiting(false),
m_retryCount(0),
m_reconnectFirstDelay(0.0),
m_reconnectPeriod(5.0),
m_noDelayFirstReconnect(true),
m_limitReconnects(true),
m_maximumRetries(10) {
setStatus(BroadcastProfile::STATUS_UNCONNECTED);
setState(NETWORKSTREAMWORKER_STATE_INIT);
// shout_init() should've already been called by now
if (!(m_pShout = shout_new())) {
errorDialog(tr("Mixxx encountered a problem"),
tr("Could not allocate shout_t"));
}
if (!(m_pShoutMetaData = shout_metadata_new())) {
errorDialog(tr("Mixxx encountered a problem"),
tr("Could not allocate shout_metadata_t"));
}
setFunctionCode(14);
if (shout_set_nonblocking(m_pShout, 1) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting non-blocking mode:"),
shout_get_error(m_pShout));
}
#ifdef SHOUT_TLS
// Libshout defaults to SHOUT_TLS_AUTO if build with SHOUT_TLS
// Sometimes autodetection fails, resulting into no metadata send
// https://github.com/mixxxdj/mixxx/issues/9599
if (shout_set_tls(m_pShout, SHOUT_TLS_DISABLED) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting tls mode:"),
shout_get_error(m_pShout));
}
#endif
}
ShoutConnection::~ShoutConnection() {
if (m_pShoutMetaData) {
shout_metadata_free(m_pShoutMetaData);
}
if (m_pShout) {
shout_close(m_pShout);
shout_free(m_pShout);
}
// Wait maximum ~4 seconds. User will get annoyed but
// if there is some network problems we let them settle
wait(4000);
// Signal user if thread doesn't die
VERIFY_OR_DEBUG_ASSERT(!isRunning()) {
qWarning() << "ShoutOutput::~ShoutOutput(): Thread didn't die.\
Ignored but file a bug report if problems rise!";
}
}
bool ShoutConnection::isConnected() {
if (m_pShout) {
m_iShoutStatus = shout_get_connected(m_pShout);
if (m_iShoutStatus == SHOUTERR_CONNECTED) {
return true;
}
}
return false;
}
// Only called when applying settings while broadcasting is active
void ShoutConnection::applySettings() {
// Do nothing if profile or Live Broadcasting is disabled
if (!m_broadcastEnabled.toBool() || !m_pProfile->getEnabled()) {
return;
}
// Setting the profile's enabled value to false tells the
// connection's thread to exit, so no need to call
// processDisconnect manually
int dStatus = getStatus();
if((dStatus == BroadcastProfile::STATUS_UNCONNECTED
|| dStatus == BroadcastProfile::STATUS_FAILURE)) {
serverConnect();
}
}
QByteArray ShoutConnection::encodeString(const QString& string) {
if (m_pTextCodec) {
return m_pTextCodec->fromUnicode(string);
}
return string.toLatin1();
}
void ShoutConnection::insertMetaData(const char *pName, const char *pValue) {
int ret = shout_metadata_add(m_pShoutMetaData, pName, pValue);
if (ret != SHOUTERR_SUCCESS) {
kLogger.warning() << "shout_metadata_add" << pName << "fails with error code" << ret;
}
}
void ShoutConnection::updateFromPreferences() {
kLogger.debug() << m_pProfile->getProfileName()
<< ": updating from preferences";
int dStatus = getStatus();
if (dStatus == BroadcastProfile::STATUS_CONNECTED ||
dStatus == BroadcastProfile::STATUS_CONNECTING) {
kLogger.warning()
<< m_pProfile->getProfileName()
<< "updateFromPreferences status:" << dStatus
<< ". Can't edit preferences when playing";
return;
}
setState(NETWORKSTREAMWORKER_STATE_BUSY);
// Delete m_encoder if it has been initialized (with maybe) different bitrate.
// delete m_encoder calls write() check if it will be exit early
DEBUG_ASSERT(m_iShoutStatus != SHOUTERR_CONNECTED);
m_encoder.reset();
m_format_is_mp3 = false;
m_format_is_ov = false;
m_format_is_aac = false;
m_protocol_is_icecast1 = false;
m_protocol_is_icecast2 = false;
m_protocol_is_shoutcast = false;
m_ogg_dynamic_update = false;
// Convert a bunch of QStrings to QByteArrays so we can get regular C char*
// strings to pass to libshout.
QString codec = m_pProfile->getMetadataCharset();
if (!codec.isEmpty()) {
QByteArray baCodec = codec.toLatin1();
m_pTextCodec = QTextCodec::codecForName(baCodec);
if (!m_pTextCodec) {
kLogger.warning()
<< "Couldn't find broadcast metadata codec for codec:" << codec
<< " defaulting to ISO-8859-1.";
} else {
// Indicates our metadata is in the provided charset.
insertMetaData("charset", baCodec.constData());
}
}
QString serverType = m_pProfile->getServertype();
QString host = m_pProfile->getHost();
int start = host.indexOf(QLatin1String("//"));
if (start == -1) {
// the host part requires preceding //.
// Without them, the path is treated relative and goes to the
// path() section.
host.prepend(QLatin1String("//"));
}
QUrl serverUrl = host;
int port = m_pProfile->getPort();
serverUrl.setPort(port);
QString mountPoint = m_pProfile->getMountpoint();
if (!mountPoint.isEmpty()) {
if (!mountPoint.startsWith('/')) {
mountPoint.prepend('/');
}
serverUrl.setPath(mountPoint);
}
QString login = m_pProfile->getLogin();
if (!login.isEmpty()) {
serverUrl.setUserName(login);
}
kLogger.debug() << "Using server URL:" << serverUrl;
QByteArray baPassword = m_pProfile->getPassword().toLatin1();
QByteArray baFormat = m_pProfile->getFormat().toLatin1();
int iBitrate = m_pProfile->getBitrate();
// Encode metadata like stream name, website, desc, genre, title/author with
// the chosen TextCodec.
QByteArray baStreamName = encodeString(m_pProfile->getStreamName());
QByteArray baStreamWebsite = encodeString(m_pProfile->getStreamWebsite());
QByteArray baStreamDesc = encodeString(m_pProfile->getStreamDesc());
QByteArray baStreamGenre = encodeString(m_pProfile->getStreamGenre());
QByteArray baStreamIRC = encodeString(m_pProfile->getStreamIRC());
QByteArray baStreamAIM = encodeString(m_pProfile->getStreamAIM());
QByteArray baStreamICQ = encodeString(m_pProfile->getStreamICQ());
// Whether the stream is public.
bool streamPublic = m_pProfile->getStreamPublic();
// Dynamic Ogg metadata update
m_ogg_dynamic_update = m_pProfile->getOggDynamicUpdate();
m_custom_metadata = m_pProfile->getEnableMetadata();
m_customTitle = m_pProfile->getCustomTitle();
m_customArtist = m_pProfile->getCustomArtist();
m_metadataFormat = m_pProfile->getMetadataFormat();
bool enableReconnect = m_pProfile->getEnableReconnect();
if (enableReconnect) {
m_reconnectFirstDelay = m_pProfile->getReconnectFirstDelay();
m_reconnectPeriod = m_pProfile->getReconnectPeriod();
m_noDelayFirstReconnect = m_pProfile->getNoDelayFirstReconnect();
m_limitReconnects = m_pProfile->getLimitReconnects();
m_maximumRetries = m_pProfile->getMaximumRetries();
} else {
m_limitReconnects = true;
m_maximumRetries = 0;
}
int format;
int protocol;
if (shout_set_host(m_pShout, serverUrl.host().toLatin1().constData())
!= SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting hostname!"), shout_get_error(m_pShout));
return;
}
if (shout_set_port(m_pShout,
static_cast<unsigned short>(serverUrl.port(BROADCAST_DEFAULT_PORT)))
!= SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting port!"), shout_get_error(m_pShout));
return;
}
if (shout_set_password(m_pShout, baPassword.constData())
!= SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting password!"), shout_get_error(m_pShout));
return;
}
if (shout_set_mount(m_pShout, serverUrl.path().toLatin1().constData())
!= SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting mount!"), shout_get_error(m_pShout));
return;
}
if (shout_set_user(m_pShout, serverUrl.userName().toLatin1().constData())
!= SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting username!"), shout_get_error(m_pShout));
return;
}
if (shout_set_name(m_pShout, baStreamName.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream name!"), shout_get_error(m_pShout));
return;
}
if (shout_set_description(m_pShout, baStreamDesc.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream description!"), shout_get_error(m_pShout));
return;
}
if (shout_set_genre(m_pShout, baStreamGenre.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream genre!"), shout_get_error(m_pShout));
return;
}
if (shout_set_url(m_pShout, baStreamWebsite.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream url!"), shout_get_error(m_pShout));
return;
}
#ifdef SHOUT_META_IRC
if (shout_set_meta(m_pShout, SHOUT_META_IRC, baStreamIRC.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream IRC!"), shout_get_error(m_pShout));
return;
}
#endif
#ifdef SHOUT_META_AIM
if (shout_set_meta(m_pShout, SHOUT_META_AIM, baStreamAIM.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream AIM!"), shout_get_error(m_pShout));
return;
}
#endif
#ifdef SHOUT_META_ICQ
if (shout_set_meta(m_pShout, SHOUT_META_ICQ, baStreamICQ.constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream ICQ!"), shout_get_error(m_pShout));
return;
}
#endif
if (shout_set_public(m_pShout, streamPublic ? 1 : 0) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream public!"), shout_get_error(m_pShout));
return;
}
m_format_is_mp3 = !qstrcmp(baFormat.constData(), ENCODING_MP3);
m_format_is_ov = !qstrcmp(baFormat.constData(), ENCODING_OGG);
m_format_is_opus = !qstrcmp(baFormat.constData(), ENCODING_OPUS);
m_format_is_aac =
(!qstrcmp(baFormat.constData(), ENCODING_AAC) ||
!qstrcmp(baFormat.constData(), ENCODING_HEAAC) ||
!qstrcmp(baFormat.constData(), ENCODING_HEAACV2));
if (m_format_is_mp3) {
format = SHOUT_FORMAT_MP3;
} else if (m_format_is_ov || m_format_is_opus) {
format = SHOUT_FORMAT_OGG;
#ifdef SHOUT_FORMAT_AAC
} else if (m_format_is_aac) {
format = SHOUT_FORMAT_AAC;
#endif
} else {
errorDialog(tr("Unknown stream encoding format!"),
tr("Use a libshout version with %1 enabled")
.arg(baFormat.constData()));
return;
}
if (shout_set_format(m_pShout, format) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting stream encoding format!"), shout_get_error(m_pShout));
return;
}
if (iBitrate < 0) {
qWarning() << "Error: unknown bit rate:" << iBitrate;
}
auto mainSamplerate = mixxx::audio::SampleRate::fromDouble(m_mainSamplerate.get());
VERIFY_OR_DEBUG_ASSERT(mainSamplerate.isValid()) {
qWarning() << "Invalid sample rate!" << mainSamplerate;
return;
}
if (m_format_is_ov && mainSamplerate == 96000) {
errorDialog(tr("Broadcasting at 96 kHz with Ogg Vorbis is not currently "
"supported. Please try a different sample rate or switch "
"to a different encoding."),
tr("See https://github.com/mixxxdj/mixxx/issues/5701 for more "
"information."));
return;
}
#ifdef __OPUS__
if (m_format_is_opus && mainSamplerate != EncoderOpus::getMainSampleRate()) {
errorDialog(
EncoderOpus::getInvalidSamplerateMessage(),
tr("Unsupported sample rate")
);
return;
}
#endif
if (shout_set_audio_info(
m_pShout, SHOUT_AI_BITRATE,
QByteArray::number(iBitrate).constData()) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting bitrate"), shout_get_error(m_pShout));
return;
}
m_protocol_is_icecast2 = serverType == BROADCAST_SERVER_ICECAST2;
m_protocol_is_shoutcast = serverType == BROADCAST_SERVER_SHOUTCAST;
m_protocol_is_icecast1 = serverType == BROADCAST_SERVER_ICECAST1;
if (m_protocol_is_icecast2) {
protocol = SHOUT_PROTOCOL_HTTP;
} else if (m_protocol_is_shoutcast) {
protocol = SHOUT_PROTOCOL_ICY;
} else if (m_protocol_is_icecast1) {
protocol = SHOUT_PROTOCOL_XAUDIOCAST;
} else {
errorDialog(tr("Error: unknown server protocol!"), shout_get_error(m_pShout));
return;
}
if (m_protocol_is_shoutcast && !(m_format_is_mp3 || m_format_is_aac)) {
errorDialog(tr("Error: Shoutcast only supports MP3 and AAC encoders"),
shout_get_error(m_pShout));
return;
}
if (shout_set_protocol(m_pShout, protocol) != SHOUTERR_SUCCESS) {
errorDialog(tr("Error setting protocol!"), shout_get_error(m_pShout));
return;
}
// Initialize m_encoder
EncoderSettingsPointer pBroadcastSettings =
std::make_shared<EncoderBroadcastSettings>(m_pProfile);
m_encoder = EncoderFactory::getFactory().createEncoder(
pBroadcastSettings, this);
QString userErrorMsg;
int ret = -1;
if (m_encoder) {
ret = m_encoder->initEncoder(mainSamplerate, &userErrorMsg);
}
// TODO(XXX): Use mixxx::audio::SampleRate instead of int in initEncoder
if (ret < 0) {
// delete m_encoder calls write() make sure it will be exit early
DEBUG_ASSERT(m_iShoutStatus != SHOUTERR_CONNECTED);
m_encoder.reset();
setState(NETWORKSTREAMWORKER_STATE_ERROR);
m_lastErrorStr = pBroadcastSettings->getFormat() + QChar(' ') +
QObject::tr(" encoder failure") + QChar('\n');
if (userErrorMsg.isEmpty()) {
m_lastErrorStr.append(QObject::tr(
"Failed to apply the selected settings."));
} else {
m_lastErrorStr.append(userErrorMsg);
}
return;
}
setState(NETWORKSTREAMWORKER_STATE_READY);
}
bool ShoutConnection::serverConnect() {
if (!m_pProfile->getEnabled()) {
return false;
}
start(QThread::HighPriority);
setState(NETWORKSTREAMWORKER_STATE_CONNECTING);
return true;
}
bool ShoutConnection::processConnect() {
kLogger.debug() << "processConnect";
// Make sure that we call updateFromPreferences always
updateFromPreferences();
if (!m_encoder) {
// updateFromPreferences failed
setStatus(BroadcastProfile::STATUS_FAILURE);
kLogger.warning() << "ShoutOutput::processConnect() returning false";
return false;
}
setStatus(BroadcastProfile::STATUS_CONNECTING);
m_iShoutFailures = 0;
m_lastErrorStr.clear();
// set to a high number to automatically update the metadata
// on the first change
m_iMetaDataLife = 31337;
// clear metadata, to make sure the first track is not skipped
// because it was sent via an previous connection (see metaDataHasChanged)
if(m_pMetaData) {
m_pMetaData.reset();
}
// If static metadata is available, we only need to send metadata one time
m_firstCall = false;
while (m_iShoutFailures < kMaxShoutFailures) {
shout_close(m_pShout);
m_iShoutStatus = shout_open(m_pShout);
if (m_iShoutStatus == SHOUTERR_SUCCESS) {
m_iShoutStatus = SHOUTERR_CONNECTED;
setState(NETWORKSTREAMWORKER_STATE_CONNECTED);
}
if ((m_iShoutStatus == SHOUTERR_BUSY) ||
(m_iShoutStatus == SHOUTERR_CONNECTED) ||
(m_iShoutStatus == SHOUTERR_SUCCESS))
{
break;
}
// SHOUTERR_INSANE self is corrupt or incorrect
// SHOUTERR_UNSUPPORTED The protocol/format combination is unsupported
// SHOUTERR_NOLOGIN The server refused login
// SHOUTERR_MALLOC There wasn't enough memory to complete the operation
if (m_iShoutStatus == SHOUTERR_INSANE ||
m_iShoutStatus == SHOUTERR_UNSUPPORTED ||
m_iShoutStatus == SHOUTERR_NOLOGIN ||
m_iShoutStatus == SHOUTERR_MALLOC) {
m_lastErrorStr = shout_get_error(m_pShout);
qWarning() << "Streaming server made fatal error. Can't continue connecting:"
<< m_lastErrorStr;
break;
}
m_iShoutFailures++;
m_lastErrorStr = shout_get_error(m_pShout);
kLogger.warning()
<< m_iShoutFailures << "/" << kMaxShoutFailures
<< "Streaming server failed connect. Failures:"
<< m_lastErrorStr;
}
// If we don't have any fatal errors let's try to connect
if ((m_iShoutStatus == SHOUTERR_BUSY ||
m_iShoutStatus == SHOUTERR_CONNECTED ||
m_iShoutStatus == SHOUTERR_SUCCESS) &&
m_iShoutFailures < kMaxShoutFailures) {
m_iShoutFailures = 0;
int timeout = 0;
while (m_iShoutStatus == SHOUTERR_BUSY &&
timeout < kConnectRetries &&
m_pProfile->getEnabled()) {
setState(NETWORKSTREAMWORKER_STATE_WAITING);
kLogger.debug() << "Connection pending. Waiting...";
m_iShoutStatus = shout_get_connected(m_pShout);
if (m_iShoutStatus != SHOUTERR_BUSY &&
m_iShoutStatus != SHOUTERR_SUCCESS &&
m_iShoutStatus != SHOUTERR_CONNECTED) {
qWarning() << "Streaming server made error:" << m_iShoutStatus;
}
// If socket is busy then we wait half second
if (m_iShoutStatus == SHOUTERR_BUSY) {
m_enabledMutex.lock();
m_waitEnabled.wait(&m_enabledMutex, 500);
m_enabledMutex.unlock();
}
++ timeout;
}
if (m_iShoutStatus == SHOUTERR_CONNECTED) {
setState(NETWORKSTREAMWORKER_STATE_READY);
kLogger.debug() << "***********Connected to streaming server...";
m_retryCount = 0;
if(m_pOutputFifo->readAvailable()) {
m_pOutputFifo->flushReadData(m_pOutputFifo->readAvailable());
}
m_threadWaiting = true;
setStatus(BroadcastProfile::STATUS_CONNECTED);
emit broadcastConnected();
kLogger.debug() << "processConnect() returning true";
return true;
} else if (m_iShoutStatus == SHOUTERR_SOCKET) {
m_lastErrorStr = "Socket error";
kLogger.warning()
<< "processConnect() socket error."
<< "Is socket already in use?";
} else if (timeout >= kConnectRetries) {
// Not translated, because shout_get_error() returns also English only
m_lastErrorStr = QStringLiteral("Connection establishment time-out");
kLogger.warning()
<< "processConnect() error:"
<< m_iShoutStatus << m_lastErrorStr;
} else if (m_pProfile->getEnabled()) {
m_lastErrorStr = shout_get_error(m_pShout);
kLogger.warning()
<< "processConnect() error:"
<< m_iShoutStatus << m_lastErrorStr;
}
}
// no connection, clean up
shout_close(m_pShout);
// delete m_encoder calls write() check if it will be exit early
DEBUG_ASSERT(m_iShoutStatus != SHOUTERR_CONNECTED);
m_encoder.reset();
if (m_pProfile->getEnabled()) {
setStatus(BroadcastProfile::STATUS_FAILURE);
} else {
setStatus(BroadcastProfile::STATUS_UNCONNECTED);
}
kLogger.debug() << "processConnect() returning false";
return false;
}
bool ShoutConnection::processDisconnect() {
kLogger.debug() << "processDisconnect()";
bool disconnected = false;
if (isConnected()) {
m_threadWaiting = false;
// We are connected but broadcast is disabled. Disconnect.
shout_close(m_pShout);
m_iShoutStatus = SHOUTERR_UNCONNECTED;
emit broadcastDisconnected();
disconnected = true;
}
// delete m_encoder calls write() check if it will be exit early
DEBUG_ASSERT(m_iShoutStatus != SHOUTERR_CONNECTED);
m_encoder.reset();
return disconnected;
}
void ShoutConnection::write(const unsigned char* header, const unsigned char* body,
int headerLen, int bodyLen) {
setFunctionCode(7);
if (!m_pShout || m_iShoutStatus != SHOUTERR_CONNECTED) {
// This happens when the decoder calls flush() and the connection is
// already down
return;
}
// Send header if there is one
if (headerLen > 0) {
if(!writeSingle(header, headerLen)) {
return;
}
}
if(!writeSingle(body, bodyLen)) {
return;
}
ssize_t queuelen = shout_queuelen(m_pShout);
if (queuelen > 0) {
kLogger.debug() << "shout_queuelen" << queuelen;
if (queuelen > kMaxNetworkCache) {
m_lastErrorStr = tr("Network cache overflow");
tryReconnect();
}
}
}
// These are not used for streaming, but the interface requires them
int ShoutConnection::tell() {
if (!m_pShout) {
return -1;
}
return -1;
}
// These are not used for streaming, but the interface requires them
void ShoutConnection::seek(int pos) {
Q_UNUSED(pos)
return;
}
// These are not used for streaming, but the interface requires them
int ShoutConnection::filelen() {
return 0;
}
bool ShoutConnection::writeSingle(const unsigned char* data, size_t len) {
setFunctionCode(8);
int ret = shout_send_raw(m_pShout, data, len);
if (ret == SHOUTERR_BUSY) {
// in case of busy, frames are queued
// try to flush queue after a short sleep
kLogger.warning() << "writeSingle() SHOUTERR_BUSY, trying again";
usleep(10000); // wait 10 ms until "busy" is over. TODO() tweak for an optimum.
// if this fails, the queue is transmitted after the next regular shout_send_raw()
(void)shout_send_raw(m_pShout, nullptr, 0);
} else if (ret < SHOUTERR_SUCCESS) {
m_lastErrorStr = shout_get_error(m_pShout);
kLogger.warning()
<< "writeSingle() error:"
<< ret << m_lastErrorStr;
if (++m_iShoutFailures > kMaxShoutFailures) {
tryReconnect();
}
return false;
} else {
m_iShoutFailures = 0;
}
return true;
}
void ShoutConnection::process(const CSAMPLE* pBuffer, const int iBufferSize) {
setFunctionCode(4);
if (!m_pProfile->getEnabled()) {
return;
}
setState(NETWORKSTREAMWORKER_STATE_BUSY);
// If we aren't connected, bail.
if (m_iShoutStatus != SHOUTERR_CONNECTED) {
return;
}
// Save a copy of the smart pointer in a local variable
// to prevent race conditions when resetting the member
// pointer while disconnecting in the worker thread!
const EncoderPointer pEncoder = m_encoder;
// If we are connected, encode the samples.
if (iBufferSize > 0 && pEncoder) {
setFunctionCode(6);
pEncoder->encodeBuffer(pBuffer, iBufferSize);
// the encoded frames are received by the write() callback.
}
// Check if track metadata has changed and if so, update.
if (metaDataHasChanged()) {
updateMetaData();
}
setState(NETWORKSTREAMWORKER_STATE_READY);
}
bool ShoutConnection::metaDataHasChanged() {
TrackPointer pTrack;
// TODO(rryan): This is latency and buffer size dependent. Should be based
// on time.
if (m_iMetaDataLife < 16) {
m_iMetaDataLife++;
return false;
}
m_iMetaDataLife = 0;
pTrack = PlayerInfo::instance().getCurrentPlayingTrack();
if (!pTrack) {
return false;
}
if (m_pMetaData) {
if (!pTrack->getId().isValid() || !m_pMetaData->getId().isValid()) {
if ((pTrack->getArtist() == m_pMetaData->getArtist()) &&
(pTrack->getTitle() == m_pMetaData->getArtist())) {
return false;
}
} else if (pTrack->getId() == m_pMetaData->getId()) {
return false;
}
}
m_pMetaData = pTrack;
return true;
}
void ShoutConnection::updateMetaData() {
setFunctionCode(5);
if (!m_pShout) {
kLogger.debug() << "updateMetaData failed, invalid m_pShout";
return;
}
if (!m_pShoutMetaData) {
kLogger.debug() << "updateMetaData failed, invalid m_pShoutMetaData";
return;
}
/**
* If track has changed and static metadata is disabled
* Send new metadata to broadcast!
* This works only for MP3 streams properly as stated in comments, see shout.h
* WARNING: Changing OGG metadata dynamically by using shout_set_metadata
* will cause stream interruptions to listeners
*
* Also note: Do not try to include Vorbis comments in OGG packages and send them to stream.
* This was done in EncoderVorbis previously and caused interruptions on track change as well
* which sounds awful to listeners.
* To conclude: Only write OGG metadata one time, i.e., if static metadata is used.
*/
// If we use either MP3 streaming, AAC streaming or OGG streaming with dynamic update of
// metadata being enabled, we want dynamic metadata changes
if (!m_custom_metadata && (m_format_is_mp3 || m_format_is_aac || m_ogg_dynamic_update)) {
if (m_pMetaData != nullptr) {
QString artist = m_pMetaData->getArtist();
QString title = m_pMetaData->getTitle();
// shoutcast uses only "song" as field for "artist - title".
// icecast2 supports separate fields for "artist" and "title",
// which will get displayed accordingly if the streamingformat and
// player supports it. ("song" is treated as an alias for "title")
//
// Note (EinWesen):
// Currently that seems to be OGG only, although it is no problem
// setting both fields for MP3, tested players do not show anything different.
// Also I do not know about icecast1. To be safe, i stick to the
// old way for those use cases.
if (!m_format_is_mp3 && m_protocol_is_icecast2) {
setFunctionCode(9);
insertMetaData("artist", encodeString(artist).constData());
insertMetaData("title", encodeString(title).constData());
} else {
// we are going to take the metadata format and replace all
// the references to $title and $artist by doing a single
// pass over the string
int replaceIndex = 0;
// Make a copy so we don't overwrite the references only
// once per streaming session.
QString metadataFinal = m_metadataFormat;
do {
// find the next occurrence
replaceIndex = metadataFinal.indexOf(
kArtistOrTitleRegex,
replaceIndex);
if (replaceIndex != -1) {
if (metadataFinal.indexOf(
kArtistRegex, replaceIndex) == replaceIndex) {
metadataFinal.replace(replaceIndex, 7, artist);
// skip to the end of the replacement
replaceIndex += artist.length();
} else {
metadataFinal.replace(replaceIndex, 6, title);
replaceIndex += title.length();
}
}
} while (replaceIndex != -1);
QByteArray baSong = encodeString(metadataFinal);
setFunctionCode(10);
insertMetaData("song", baSong.constData());
}
setFunctionCode(11);
int ret = shout_set_metadata(m_pShout, m_pShoutMetaData);
if (ret != SHOUTERR_SUCCESS) {
kLogger.warning() << "shout_set_metadata fails with error code" << ret;
}
}
} else {
// Otherwise we might use static metadata
// If we use static metadata, we only need to call the following line once
if (m_custom_metadata && !m_firstCall) {
// see comment above...
if (!m_format_is_mp3 && m_protocol_is_icecast2) {
setFunctionCode(12);
insertMetaData("artist", encodeString(m_customArtist).constData());
insertMetaData("title", encodeString(m_customTitle).constData());
} else {
QByteArray baCustomSong = encodeString(m_customArtist.isEmpty() ? m_customTitle : m_customArtist + " - " + m_customTitle);
insertMetaData("song", baCustomSong.constData());
}
setFunctionCode(13);
shout_set_metadata(m_pShout, m_pShoutMetaData);
m_firstCall = true;
}
}
}
void ShoutConnection::errorDialog(const QString& text, const QString& detailedError) {
qWarning() << "Streaming error: " << detailedError;
ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
props->setType(DLG_WARNING);
props->setTitle(tr("Connection error"));
props->setText(tr("One of the Live Broadcasting connections raised this error:<br>"
"<b>Error with connection '%1':</b><br>")
.arg(profile()->getProfileName()) + text);
props->setDetails(detailedError);
props->setKey(detailedError); // To prevent multiple windows for the same error
props->setDefaultButton(QMessageBox::Close);
props->setModal(false);
ErrorDialogHandler::instance()->requestErrorDialog(props);
setState(NETWORKSTREAMWORKER_STATE_ERROR);
}
void ShoutConnection::infoDialog(const QString& text, const QString& detailedInfo) {
ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
props->setType(DLG_INFO);
props->setTitle(tr("Connection message"));
props->setText(tr("<b>Message from Live Broadcasting connection '%1':</b><br>")
.arg(profile()->getProfileName()) + text);
props->setDetails(detailedInfo);
props->setKey(text + detailedInfo);
props->setDefaultButton(QMessageBox::Close);
props->setModal(false);
ErrorDialogHandler::instance()->requestErrorDialog(props);
}
bool ShoutConnection::waitForRetry() {
if (m_limitReconnects &&
m_retryCount >= m_maximumRetries) {
return false;
}
++m_retryCount;
kLogger.debug() << "waitForRetry()" << m_retryCount << "/" << m_maximumRetries;
double delay;
if (m_retryCount == 1) {
delay = m_reconnectFirstDelay;
} else {
delay = m_reconnectPeriod;
}
if (delay > 0) {
m_enabledMutex.lock();
m_waitEnabled.wait(&m_enabledMutex, static_cast<unsigned long>(delay * 1000));
m_enabledMutex.unlock();
if (!m_pProfile->getEnabled()) {
return false;
}
}
return true;
}
void ShoutConnection::tryReconnect() {
QString originalErrorStr = m_lastErrorStr;
setStatus(BroadcastProfile::STATUS_FAILURE);
processDisconnect();
while (waitForRetry()) {
if (processConnect()) {
break;
}
}
if (getStatus() == BroadcastProfile::STATUS_FAILURE) {
QString errorText;
if (m_retryCount > 0) {
errorText = tr("Lost connection to streaming server and %1 attempts to reconnect have failed.")
.arg(m_retryCount);
} else {
errorText = tr("Lost connection to streaming server.");
}
errorDialog(errorText,
originalErrorStr + "\n" +
m_lastErrorStr + "\n" +
tr("Please check your connection to the Internet."));
}
}
void ShoutConnection::outputAvailable() {
m_readSema.release();
}
void ShoutConnection::setOutputFifo(QSharedPointer<FIFO<CSAMPLE>> pOutputFifo) {
m_pOutputFifo = pOutputFifo;
}
QSharedPointer<FIFO<CSAMPLE>> ShoutConnection::getOutputFifo() {
return m_pOutputFifo;
}
bool ShoutConnection::threadWaiting() {
return atomicLoadRelaxed(m_threadWaiting);
}