ShoutOutputPlugin.cxx 11 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 The Music Player Daemon Project
3
 * http://www.musicpd.org
4 5 6 7 8 9 10 11 12 13
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 19
 */

20
#include "config.h"
21
#include "ShoutOutputPlugin.hxx"
22
#include "../OutputAPI.hxx"
23
#include "encoder/EncoderInterface.hxx"
24 25
#include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx"
26
#include "util/RuntimeError.hxx"
27
#include "util/Domain.hxx"
28
#include "Log.hxx"
Warren Dukes's avatar
Warren Dukes committed
29

30 31
#include <shout/shout.h>

32 33
#include <stdexcept>

34
#include <assert.h>
35
#include <stdlib.h>
Max Kellermann's avatar
Max Kellermann committed
36
#include <string.h>
37
#include <stdio.h>
38

39
static constexpr unsigned DEFAULT_CONN_TIMEOUT = 2;
40

41
struct ShoutOutput final : AudioOutput {
42 43 44
	shout_t *shout_conn;
	shout_metadata_t *shout_meta;

45 46
	PreparedEncoder *prepared_encoder = nullptr;
	Encoder *encoder;
47

48 49
	float quality = -2.0;
	int bitrate = -1;
50

51
	int timeout = DEFAULT_CONN_TIMEOUT;
52

53
	uint8_t buffer[32768];
54

55
	explicit ShoutOutput(const ConfigBlock &block);
56
	~ShoutOutput();
57

58
	static AudioOutput *Create(EventLoop &event_loop,
59
				   const ConfigBlock &block);
60

61 62
	void Open(AudioFormat &audio_format) override;
	void Close() noexcept override;
63

64 65 66 67 68
	std::chrono::steady_clock::duration Delay() const noexcept override;
	void SendTag(const Tag &tag) override;
	size_t Play(const void *chunk, size_t size) override;
	void Cancel() noexcept override;
	bool Pause() noexcept override;
69 70 71

private:
	void WritePage();
72 73
};

Max Kellermann's avatar
Max Kellermann committed
74
static int shout_init_count;
75

76
static constexpr Domain shout_output_domain("shout_output");
77

78 79
static const char *
require_block_string(const ConfigBlock &block, const char *name)
80
{
81 82 83 84
	const char *value = block.GetBlockValue(name);
	if (value == nullptr)
		throw FormatRuntimeError("no \"%s\" defined for shout device defined "
					 "at line %d\n", name, block.line);
85

86
	return value;
87 88
}

89
static const EncoderPlugin *
90
shout_encoder_plugin_get(const char *name)
91
{
92 93 94 95
	if (strcmp(name, "ogg") == 0)
		name = "vorbis";
	else if (strcmp(name, "mp3") == 0)
		name = "lame";
96

97
	return encoder_plugin_get(name);
98
}
99

100 101 102 103 104 105 106 107 108 109 110 111
static void
ShoutSetAudioInfo(shout_t *shout_conn, const AudioFormat &audio_format)
{
	char temp[11];

	snprintf(temp, sizeof(temp), "%u", audio_format.channels);
	shout_set_audio_info(shout_conn, SHOUT_AI_CHANNELS, temp);

	snprintf(temp, sizeof(temp), "%u", audio_format.sample_rate);
	shout_set_audio_info(shout_conn, SHOUT_AI_SAMPLERATE, temp);
}

112
ShoutOutput::ShoutOutput(const ConfigBlock &block)
113
	:AudioOutput(FLAG_PAUSE),
114 115
	 shout_conn(shout_new()),
	 shout_meta(shout_metadata_new())
116
{
117
	NeedFullyDefinedAudioFormat();
118

119 120 121
	const char *host = require_block_string(block, "host");
	const char *mount = require_block_string(block, "mount");
	unsigned port = block.GetBlockValue("port", 0u);
122 123
	if (port == 0)
		throw std::runtime_error("shout port must be configured");
124

125 126
	const char *passwd = require_block_string(block, "password");
	const char *name = require_block_string(block, "name");
127

128
	bool is_public = block.GetBlockValue("public", false);
129

130
	const char *user = block.GetBlockValue("user", "source");
131

132
	const char *value = block.GetBlockValue("quality");
133
	if (value != nullptr) {
134
		char *test;
135
		quality = strtod(value, &test);
136

137 138 139 140
		if (*test != '\0' || quality < -1.0 || quality > 10.0)
			throw FormatRuntimeError("shout quality \"%s\" is not a number in the "
						 "range -1 to 10",
						 value);
141

142 143 144
		if (block.GetBlockValue("bitrate") != nullptr)
			throw std::runtime_error("quality and bitrate are "
						 "both defined");
Avuton Olrich's avatar
Avuton Olrich committed
145
	} else {
146
		value = block.GetBlockValue("bitrate");
147 148
		if (value == nullptr)
			throw std::runtime_error("neither bitrate nor quality defined");
149

150
		char *test;
151
		bitrate = strtol(value, &test, 10);
152

153 154
		if (*test != '\0' || bitrate <= 0)
			throw std::runtime_error("bitrate must be a positive integer");
155 156
	}

157
	const char *encoding = block.GetBlockValue("encoder", nullptr);
158
	if (encoding == nullptr)
159
		encoding = block.GetBlockValue("encoding", "vorbis");
160
	const auto encoder_plugin = shout_encoder_plugin_get(encoding);
161 162 163
	if (encoder_plugin == nullptr)
		throw FormatRuntimeError("couldn't find shout encoder plugin \"%s\"",
					 encoding);
164

165
	prepared_encoder = encoder_init(*encoder_plugin, block);
166

167
	unsigned shout_format;
168 169 170 171 172
	if (strcmp(encoding, "mp3") == 0 || strcmp(encoding, "lame") == 0)
		shout_format = SHOUT_FORMAT_MP3;
	else
		shout_format = SHOUT_FORMAT_OGG;

173
	unsigned protocol;
174
	value = block.GetBlockValue("protocol");
175
	if (value != nullptr) {
176
		if (0 == strcmp(value, "shoutcast") &&
177 178 179 180
		    0 != strcmp(encoding, "mp3"))
			throw FormatRuntimeError("you cannot stream \"%s\" to shoutcast, use mp3",
						 encoding);
		else if (0 == strcmp(value, "shoutcast"))
181
			protocol = SHOUT_PROTOCOL_ICY;
182
		else if (0 == strcmp(value, "icecast1"))
183
			protocol = SHOUT_PROTOCOL_XAUDIOCAST;
184
		else if (0 == strcmp(value, "icecast2"))
185
			protocol = SHOUT_PROTOCOL_HTTP;
186 187 188 189
		else
			throw FormatRuntimeError("shout protocol \"%s\" is not \"shoutcast\" or "
						 "\"icecast1\"or \"icecast2\"",
						 value);
190 191 192 193
	} else {
		protocol = SHOUT_PROTOCOL_HTTP;
	}

194 195 196 197 198 199 200 201
	if (shout_set_host(shout_conn, host) != SHOUTERR_SUCCESS ||
	    shout_set_port(shout_conn, port) != SHOUTERR_SUCCESS ||
	    shout_set_password(shout_conn, passwd) != SHOUTERR_SUCCESS ||
	    shout_set_mount(shout_conn, mount) != SHOUTERR_SUCCESS ||
	    shout_set_name(shout_conn, name) != SHOUTERR_SUCCESS ||
	    shout_set_user(shout_conn, user) != SHOUTERR_SUCCESS ||
	    shout_set_public(shout_conn, is_public) != SHOUTERR_SUCCESS ||
	    shout_set_format(shout_conn, shout_format)
Avuton Olrich's avatar
Avuton Olrich committed
202
	    != SHOUTERR_SUCCESS ||
203
	    shout_set_protocol(shout_conn, protocol) != SHOUTERR_SUCCESS ||
204 205
	    shout_set_agent(shout_conn, "MPD") != SHOUTERR_SUCCESS)
		throw std::runtime_error(shout_get_error(shout_conn));
206

207
	/* optional paramters */
208
	timeout = block.GetBlockValue("timeout", DEFAULT_CONN_TIMEOUT);
209

210
	value = block.GetBlockValue("genre");
211 212
	if (value != nullptr && shout_set_genre(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
213

214
	value = block.GetBlockValue("description");
215 216
	if (value != nullptr && shout_set_description(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
217

218
	value = block.GetBlockValue("url");
219 220
	if (value != nullptr && shout_set_url(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
221

222 223
	{
		char temp[11];
224 225 226
		if (quality >= -1.0) {
			snprintf(temp, sizeof(temp), "%2.2f", quality);
			shout_set_audio_info(shout_conn, SHOUT_AI_QUALITY,
Avuton Olrich's avatar
Avuton Olrich committed
227 228
					     temp);
		} else {
229 230
			snprintf(temp, sizeof(temp), "%d", bitrate);
			shout_set_audio_info(shout_conn, SHOUT_AI_BITRATE,
Avuton Olrich's avatar
Avuton Olrich committed
231
					     temp);
232 233
		}
	}
234
}
235

236 237 238 239 240 241 242 243 244 245 246 247
ShoutOutput::~ShoutOutput()
{
	if (shout_meta != nullptr)
		shout_metadata_free(shout_meta);
	if (shout_conn != nullptr)
		shout_free(shout_conn);

	shout_init_count--;
	if (shout_init_count == 0)
		shout_shutdown();

	delete prepared_encoder;
248
}
249

250
AudioOutput *
251
ShoutOutput::Create(EventLoop &, const ConfigBlock &block)
252
{
253 254 255 256 257
	if (shout_init_count == 0)
		shout_init();

	shout_init_count++;

258
	return new ShoutOutput(block);
259 260
}

261
static void
262
HandleShoutError(shout_t *shout_conn, int err)
Avuton Olrich's avatar
Avuton Olrich committed
263 264
{
	switch (err) {
265 266
	case SHOUTERR_SUCCESS:
		break;
267

268 269
	case SHOUTERR_UNCONNECTED:
	case SHOUTERR_SOCKET:
270
		throw FormatRuntimeError("Lost shout connection to %s:%i: %s",
271 272 273
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
274

275
	default:
276
		throw FormatRuntimeError("connection to %s:%i error: %s",
277 278 279
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
280 281 282
	}
}

283 284 285
static void
EncoderToShout(shout_t *shout_conn, Encoder &encoder,
	       unsigned char *buffer, size_t buffer_size)
Avuton Olrich's avatar
Avuton Olrich committed
286
{
287
	while (true) {
288
		size_t nbytes = encoder.Read(buffer, buffer_size);
289
		if (nbytes == 0)
290
			return;
291

292 293
		int err = shout_send(shout_conn, buffer, nbytes);
		HandleShoutError(shout_conn, err);
294
	}
295
}
296

297 298 299 300 301 302
void
ShoutOutput::WritePage()
{
	assert(encoder != nullptr);

	EncoderToShout(shout_conn, *encoder, buffer, sizeof(buffer));
303 304
}

305
void
306
ShoutOutput::Close() noexcept
Avuton Olrich's avatar
Avuton Olrich committed
307
{
308 309
	try {
		encoder->End();
310
		WritePage();
311 312
	} catch (const std::runtime_error &) {
		/* ignore */
313
	}
314

315 316
	delete encoder;

317 318
	if (shout_get_connected(shout_conn) != SHOUTERR_UNCONNECTED &&
	    shout_close(shout_conn) != SHOUTERR_SUCCESS) {
319 320
		FormatWarning(shout_output_domain,
			      "problem closing connection to shout server: %s",
321
			      shout_get_error(shout_conn));
322 323 324
	}
}

325
void
326
ShoutOutput::Cancel() noexcept
Avuton Olrich's avatar
Avuton Olrich committed
327
{
328
	/* needs to be implemented for shout */
329 330
}

331
static void
332
ShoutOpen(shout_t *shout_conn)
Avuton Olrich's avatar
Avuton Olrich committed
333
{
334
	switch (shout_open(shout_conn)) {
335 336
	case SHOUTERR_SUCCESS:
	case SHOUTERR_CONNECTED:
337
		break;
338

339
	default:
340
		throw FormatRuntimeError("problem opening connection to shout server %s:%i: %s",
341 342 343
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
344
	}
345 346
}

347 348
void
ShoutOutput::Open(AudioFormat &audio_format)
349
{
350
	encoder = prepared_encoder->Open(audio_format);
351

352
	try {
353
		ShoutSetAudioInfo(shout_conn, audio_format);
354 355 356 357
		ShoutOpen(shout_conn);
		WritePage();
	} catch (...) {
		delete encoder;
358
		throw;
359
	}
360 361
}

362
std::chrono::steady_clock::duration
363
ShoutOutput::Delay() const noexcept
364
{
365
	int delay = shout_delay(shout_conn);
366 367 368
	if (delay < 0)
		delay = 0;

369
	return std::chrono::milliseconds(delay);
370 371
}

372
size_t
373
ShoutOutput::Play(const void *chunk, size_t size)
Avuton Olrich's avatar
Avuton Olrich committed
374
{
375
	encoder->Write(chunk, size);
376
	WritePage();
377
	return size;
378 379
}

380
bool
381
ShoutOutput::Pause() noexcept
382
{
383
	static char silence[1020];
384

385 386
	try {
		encoder->Write(silence, sizeof(silence));
387
		WritePage();
388 389 390 391 392
	} catch (const std::runtime_error &) {
		return false;
	}

	return true;
393 394
}

395
static void
396
shout_tag_to_metadata(const Tag &tag, char *dest, size_t size)
397 398 399 400 401 402 403
{
	char artist[size];
	char title[size];

	artist[0] = 0;
	title[0] = 0;

404
	for (const auto &item : tag) {
405
		switch (item.type) {
406
		case TAG_ARTIST:
407
			strncpy(artist, item.value, size);
408
			break;
409
		case TAG_TITLE:
410
			strncpy(title, item.value, size);
411 412 413 414 415 416 417
			break;

		default:
			break;
		}
	}

418
	snprintf(dest, size, "%s - %s", artist, title);
419 420
}

421 422
void
ShoutOutput::SendTag(const Tag &tag)
Avuton Olrich's avatar
Avuton Olrich committed
423
{
424
	if (encoder->ImplementsTag()) {
425 426
		/* encoder plugin supports stream tags */

427
		encoder->PreTag();
428
		WritePage();
429
		encoder->SendTag(tag);
430 431 432 433
	} else {
		/* no stream tag support: fall back to icy-metadata */
		char song[1024];
		shout_tag_to_metadata(tag, song, sizeof(song));
434

435 436 437
		shout_metadata_add(shout_meta, "song", song);
		if (SHOUTERR_SUCCESS != shout_set_metadata(shout_conn,
							   shout_meta)) {
438 439
			LogWarning(shout_output_domain,
				   "error setting shout metadata");
440 441
		}
	}
442

443
	WritePage();
444 445
}

446
const struct AudioOutputPlugin shout_output_plugin = {
447 448
	"shout",
	nullptr,
449
	&ShoutOutput::Create,
450
	nullptr,
451
};