ShoutOutputPlugin.cxx 9.46 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
#include "encoder/Configured.hxx"
25
#include "util/RuntimeError.hxx"
26
#include "util/Domain.hxx"
27
#include "util/ScopeExit.hxx"
28
#include "util/StringAPI.hxx"
29
#include "Log.hxx"
Warren Dukes's avatar
Warren Dukes committed
30

31 32
#include <shout/shout.h>

33
#include <stdexcept>
34
#include <memory>
35

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

41
static constexpr unsigned DEFAULT_CONN_TIMEOUT = 2;
42

43
struct ShoutOutput final : AudioOutput {
44 45
	shout_t *shout_conn;

46
	std::unique_ptr<PreparedEncoder> prepared_encoder;
47
	Encoder *encoder;
48

49
	int timeout = DEFAULT_CONN_TIMEOUT;
50

51
	uint8_t buffer[32768];
52

53
	explicit ShoutOutput(const ConfigBlock &block);
54
	~ShoutOutput();
55

56
	static AudioOutput *Create(EventLoop &event_loop,
57
				   const ConfigBlock &block);
58

59 60
	void Open(AudioFormat &audio_format) override;
	void Close() noexcept override;
61

62 63 64 65
	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;
66
	bool Pause() override;
67 68 69

private:
	void WritePage();
70 71
};

Max Kellermann's avatar
Max Kellermann committed
72
static int shout_init_count;
73

74
static constexpr Domain shout_output_domain("shout_output");
75

76 77
static const char *
require_block_string(const ConfigBlock &block, const char *name)
78
{
79 80 81 82
	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);
83

84
	return value;
85 86
}

87 88 89 90 91 92 93 94 95 96 97 98
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);
}

99
ShoutOutput::ShoutOutput(const ConfigBlock &block)
100
	:AudioOutput(FLAG_PAUSE),
101
	 shout_conn(shout_new()),
102
	 prepared_encoder(CreateConfiguredEncoder(block, true))
103
{
104
	NeedFullyDefinedAudioFormat();
105

106 107 108
	const char *host = require_block_string(block, "host");
	const char *mount = require_block_string(block, "mount");
	unsigned port = block.GetBlockValue("port", 0u);
109 110
	if (port == 0)
		throw std::runtime_error("shout port must be configured");
111

112 113
	const char *passwd = require_block_string(block, "password");
	const char *name = require_block_string(block, "name");
114

115
	bool is_public = block.GetBlockValue("public", false);
116

117
	const char *user = block.GetBlockValue("user", "source");
118

119 120
	const char *const mime_type = prepared_encoder->GetMimeType();

121
	unsigned shout_format;
122
	if (StringIsEqual(mime_type, "audio/mpeg"))
123 124 125 126
		shout_format = SHOUT_FORMAT_MP3;
	else
		shout_format = SHOUT_FORMAT_OGG;

127
	unsigned protocol;
128
	const char *value = block.GetBlockValue("protocol");
129
	if (value != nullptr) {
130
		if (0 == strcmp(value, "shoutcast") &&
131
		    !StringIsEqual(mime_type, "audio/mpeg"))
132
			throw FormatRuntimeError("you cannot stream \"%s\" to shoutcast, use mp3",
133
						 mime_type);
134
		else if (0 == strcmp(value, "shoutcast"))
135
			protocol = SHOUT_PROTOCOL_ICY;
136
		else if (0 == strcmp(value, "icecast1"))
137
			protocol = SHOUT_PROTOCOL_XAUDIOCAST;
138
		else if (0 == strcmp(value, "icecast2"))
139
			protocol = SHOUT_PROTOCOL_HTTP;
140 141 142 143
		else
			throw FormatRuntimeError("shout protocol \"%s\" is not \"shoutcast\" or "
						 "\"icecast1\"or \"icecast2\"",
						 value);
144 145 146 147
	} else {
		protocol = SHOUT_PROTOCOL_HTTP;
	}

148 149 150 151 152 153 154 155
	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
156
	    != SHOUTERR_SUCCESS ||
157
	    shout_set_protocol(shout_conn, protocol) != SHOUTERR_SUCCESS ||
158 159
	    shout_set_agent(shout_conn, "MPD") != SHOUTERR_SUCCESS)
		throw std::runtime_error(shout_get_error(shout_conn));
160

161
	/* optional paramters */
162
	timeout = block.GetBlockValue("timeout", DEFAULT_CONN_TIMEOUT);
163

164
	value = block.GetBlockValue("genre");
165 166
	if (value != nullptr && shout_set_genre(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
167

168
	value = block.GetBlockValue("description");
169 170
	if (value != nullptr && shout_set_description(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
171

172
	value = block.GetBlockValue("url");
173 174
	if (value != nullptr && shout_set_url(shout_conn, value))
		throw std::runtime_error(shout_get_error(shout_conn));
175

176 177 178 179 180 181 182
	value = block.GetBlockValue("quality");
	if (value != nullptr)
		shout_set_audio_info(shout_conn, SHOUT_AI_QUALITY, value);

	value = block.GetBlockValue("bitrate");
	if (value != nullptr)
		shout_set_audio_info(shout_conn, SHOUT_AI_BITRATE, value);
183
}
184

185 186 187 188 189 190 191 192
ShoutOutput::~ShoutOutput()
{
	if (shout_conn != nullptr)
		shout_free(shout_conn);

	shout_init_count--;
	if (shout_init_count == 0)
		shout_shutdown();
193
}
194

195
AudioOutput *
196
ShoutOutput::Create(EventLoop &, const ConfigBlock &block)
197
{
198 199 200 201 202
	if (shout_init_count == 0)
		shout_init();

	shout_init_count++;

203
	return new ShoutOutput(block);
204 205
}

206
static void
207
HandleShoutError(shout_t *shout_conn, int err)
Avuton Olrich's avatar
Avuton Olrich committed
208 209
{
	switch (err) {
210 211
	case SHOUTERR_SUCCESS:
		break;
212

213 214
	case SHOUTERR_UNCONNECTED:
	case SHOUTERR_SOCKET:
215
		throw FormatRuntimeError("Lost shout connection to %s:%i: %s",
216 217 218
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
219

220
	default:
221
		throw FormatRuntimeError("connection to %s:%i error: %s",
222 223 224
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
225 226 227
	}
}

228 229 230
static void
EncoderToShout(shout_t *shout_conn, Encoder &encoder,
	       unsigned char *buffer, size_t buffer_size)
Avuton Olrich's avatar
Avuton Olrich committed
231
{
232
	while (true) {
233
		size_t nbytes = encoder.Read(buffer, buffer_size);
234
		if (nbytes == 0)
235
			return;
236

237 238
		int err = shout_send(shout_conn, buffer, nbytes);
		HandleShoutError(shout_conn, err);
239
	}
240
}
241

242 243 244 245 246 247
void
ShoutOutput::WritePage()
{
	assert(encoder != nullptr);

	EncoderToShout(shout_conn, *encoder, buffer, sizeof(buffer));
248 249
}

250
void
251
ShoutOutput::Close() noexcept
Avuton Olrich's avatar
Avuton Olrich committed
252
{
253 254
	try {
		encoder->End();
255
		WritePage();
256 257
	} catch (const std::runtime_error &) {
		/* ignore */
258
	}
259

260 261
	delete encoder;

262 263
	if (shout_get_connected(shout_conn) != SHOUTERR_UNCONNECTED &&
	    shout_close(shout_conn) != SHOUTERR_SUCCESS) {
264 265
		FormatWarning(shout_output_domain,
			      "problem closing connection to shout server: %s",
266
			      shout_get_error(shout_conn));
267 268 269
	}
}

270
void
271
ShoutOutput::Cancel() noexcept
Avuton Olrich's avatar
Avuton Olrich committed
272
{
273
	/* needs to be implemented for shout */
274 275
}

276
static void
277
ShoutOpen(shout_t *shout_conn)
Avuton Olrich's avatar
Avuton Olrich committed
278
{
279
	switch (shout_open(shout_conn)) {
280 281
	case SHOUTERR_SUCCESS:
	case SHOUTERR_CONNECTED:
282
		break;
283

284
	default:
285
		throw FormatRuntimeError("problem opening connection to shout server %s:%i: %s",
286 287 288
					 shout_get_host(shout_conn),
					 shout_get_port(shout_conn),
					 shout_get_error(shout_conn));
289
	}
290 291
}

292 293
void
ShoutOutput::Open(AudioFormat &audio_format)
294
{
295
	encoder = prepared_encoder->Open(audio_format);
296

297
	try {
298
		ShoutSetAudioInfo(shout_conn, audio_format);
299 300 301 302
		ShoutOpen(shout_conn);
		WritePage();
	} catch (...) {
		delete encoder;
303
		throw;
304
	}
305 306
}

307
std::chrono::steady_clock::duration
308
ShoutOutput::Delay() const noexcept
309
{
310
	int delay = shout_delay(shout_conn);
311 312 313
	if (delay < 0)
		delay = 0;

314
	return std::chrono::milliseconds(delay);
315 316
}

317
size_t
318
ShoutOutput::Play(const void *chunk, size_t size)
Avuton Olrich's avatar
Avuton Olrich committed
319
{
320
	encoder->Write(chunk, size);
321
	WritePage();
322
	return size;
323 324
}

325
bool
326
ShoutOutput::Pause()
327
{
328
	static char silence[1020];
329

330 331
	encoder->Write(silence, sizeof(silence));
	WritePage();
332 333

	return true;
334 335
}

336
static void
337
shout_tag_to_metadata(const Tag &tag, char *dest, size_t size)
338
{
339 340
	const char *artist = tag.GetValue(TAG_ARTIST);
	const char *title = tag.GetValue(TAG_TITLE);
341

342 343 344
	snprintf(dest, size, "%s - %s",
		 artist != nullptr ? artist : "",
		 title != nullptr ? title : "");
345 346
}

347 348
void
ShoutOutput::SendTag(const Tag &tag)
Avuton Olrich's avatar
Avuton Olrich committed
349
{
350
	if (encoder->ImplementsTag()) {
351 352
		/* encoder plugin supports stream tags */

353
		encoder->PreTag();
354
		WritePage();
355
		encoder->SendTag(tag);
356 357
	} else {
		/* no stream tag support: fall back to icy-metadata */
358 359 360 361

		const auto meta = shout_metadata_new();
		AtScopeExit(meta) { shout_metadata_free(meta); };

362 363
		char song[1024];
		shout_tag_to_metadata(tag, song, sizeof(song));
364

365 366
		shout_metadata_add(meta, "song", song);
		if (SHOUTERR_SUCCESS != shout_set_metadata(shout_conn, meta)) {
367 368
			LogWarning(shout_output_domain,
				   "error setting shout metadata");
369 370
		}
	}
371

372
	WritePage();
373 374
}

375
const struct AudioOutputPlugin shout_output_plugin = {
376 377
	"shout",
	nullptr,
378
	&ShoutOutput::Create,
379
	nullptr,
380
};