encoder/opus: add optional stream chaining support
support for chaining ogg opus streams to enable changing stream' metadata on the fly. currently support is opt-in (enabled by additional option) because lots of clients can't handle this properly yet.
This commit is contained in:
parent
1628d801f9
commit
47d1d3c855
2
NEWS
2
NEWS
|
@ -26,6 +26,8 @@ ver 0.21 (not yet released)
|
||||||
- sndio: remove support for the broken RoarAudio sndio emulation
|
- sndio: remove support for the broken RoarAudio sndio emulation
|
||||||
* mixer
|
* mixer
|
||||||
- sndio: new mixer plugin
|
- sndio: new mixer plugin
|
||||||
|
* encoder
|
||||||
|
- opus: support for sending metadata using ogg stream chaining
|
||||||
* require GCC 5.0
|
* require GCC 5.0
|
||||||
|
|
||||||
ver 0.20.18 (2018/02/24)
|
ver 0.20.18 (2018/02/24)
|
||||||
|
|
17
doc/user.xml
17
doc/user.xml
|
@ -3251,6 +3251,23 @@ run</programlisting>
|
||||||
(the default), "voice" and "music".
|
(the default), "voice" and "music".
|
||||||
</entry>
|
</entry>
|
||||||
</row>
|
</row>
|
||||||
|
|
||||||
|
<row>
|
||||||
|
<entry>
|
||||||
|
<varname>opustags</varname>
|
||||||
|
<parameter>yes|no</parameter>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
Configures how metadata is interleaved into the stream.
|
||||||
|
If set to <parameter>yes</parameter>, then metadata
|
||||||
|
is inserted using ogg stream chaining, as specified
|
||||||
|
in <ulink url="https://tools.ietf.org/html/rfc7845.html#section-7.2">RFC
|
||||||
|
7845</ulink>. If set to <parameter>no</parameter>
|
||||||
|
(the default), then ogg stream chaining is avoided
|
||||||
|
and other output-dependent method is used, if
|
||||||
|
available.
|
||||||
|
</entry>
|
||||||
|
</row>
|
||||||
</tbody>
|
</tbody>
|
||||||
</tgroup>
|
</tgroup>
|
||||||
</informaltable>
|
</informaltable>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
#include "config/ConfigError.hxx"
|
#include "config/ConfigError.hxx"
|
||||||
#include "util/Alloc.hxx"
|
#include "util/Alloc.hxx"
|
||||||
#include "system/ByteOrder.hxx"
|
#include "system/ByteOrder.hxx"
|
||||||
|
#include "util/StringUtil.hxx"
|
||||||
|
|
||||||
#include <opus.h>
|
#include <opus.h>
|
||||||
#include <ogg/ogg.h>
|
#include <ogg/ogg.h>
|
||||||
|
@ -55,27 +56,30 @@ class OpusEncoder final : public OggEncoder {
|
||||||
ogg_int64_t granulepos = 0;
|
ogg_int64_t granulepos = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc);
|
OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining);
|
||||||
~OpusEncoder() override;
|
~OpusEncoder() override;
|
||||||
|
|
||||||
/* virtual methods from class Encoder */
|
/* virtual methods from class Encoder */
|
||||||
void End() override;
|
void End() override;
|
||||||
void Write(const void *data, size_t length) override;
|
void Write(const void *data, size_t length) override;
|
||||||
|
|
||||||
size_t Read(void *dest, size_t length) override;
|
void PreTag() override;
|
||||||
|
void SendTag(const Tag &tag) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void DoEncode(bool eos);
|
void DoEncode(bool eos);
|
||||||
void WriteSilence(unsigned fill_frames);
|
void WriteSilence(unsigned fill_frames);
|
||||||
|
|
||||||
|
void GenerateHeaders(const Tag *tag);
|
||||||
void GenerateHead();
|
void GenerateHead();
|
||||||
void GenerateTags();
|
void GenerateTags(const Tag *tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
class PreparedOpusEncoder final : public PreparedEncoder {
|
class PreparedOpusEncoder final : public PreparedEncoder {
|
||||||
opus_int32 bitrate;
|
opus_int32 bitrate;
|
||||||
int complexity;
|
int complexity;
|
||||||
int signal;
|
int signal;
|
||||||
|
const bool chaining;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PreparedOpusEncoder(const ConfigBlock &block);
|
PreparedOpusEncoder(const ConfigBlock &block);
|
||||||
|
@ -89,6 +93,7 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
PreparedOpusEncoder::PreparedOpusEncoder(const ConfigBlock &block)
|
PreparedOpusEncoder::PreparedOpusEncoder(const ConfigBlock &block)
|
||||||
|
:chaining(block.GetBlockValue("opustags", false))
|
||||||
{
|
{
|
||||||
const char *value = block.GetBlockValue("bitrate", "auto");
|
const char *value = block.GetBlockValue("bitrate", "auto");
|
||||||
if (strcmp(value, "auto") == 0)
|
if (strcmp(value, "auto") == 0)
|
||||||
|
@ -124,8 +129,8 @@ opus_encoder_init(const ConfigBlock &block)
|
||||||
return new PreparedOpusEncoder(block);
|
return new PreparedOpusEncoder(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc)
|
OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining)
|
||||||
:OggEncoder(false),
|
:OggEncoder(_chaining),
|
||||||
audio_format(_audio_format),
|
audio_format(_audio_format),
|
||||||
frame_size(_audio_format.GetFrameSize()),
|
frame_size(_audio_format.GetFrameSize()),
|
||||||
buffer_frames(_audio_format.sample_rate / 50),
|
buffer_frames(_audio_format.sample_rate / 50),
|
||||||
|
@ -134,6 +139,7 @@ OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc)
|
||||||
enc(_enc)
|
enc(_enc)
|
||||||
{
|
{
|
||||||
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
|
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
|
||||||
|
GenerateHeaders(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
Encoder *
|
Encoder *
|
||||||
|
@ -171,7 +177,7 @@ PreparedOpusEncoder::Open(AudioFormat &audio_format)
|
||||||
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
|
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
|
||||||
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal));
|
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal));
|
||||||
|
|
||||||
return new OpusEncoder(audio_format, enc);
|
return new OpusEncoder(audio_format, enc, chaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
OpusEncoder::~OpusEncoder()
|
OpusEncoder::~OpusEncoder()
|
||||||
|
@ -183,24 +189,24 @@ OpusEncoder::~OpusEncoder()
|
||||||
void
|
void
|
||||||
OpusEncoder::DoEncode(bool eos)
|
OpusEncoder::DoEncode(bool eos)
|
||||||
{
|
{
|
||||||
assert(buffer_position == buffer_size);
|
assert(buffer_position == buffer_size || eos);
|
||||||
|
|
||||||
opus_int32 result =
|
opus_int32 result =
|
||||||
audio_format.format == SampleFormat::S16
|
audio_format.format == SampleFormat::S16
|
||||||
? opus_encode(enc,
|
? opus_encode(enc,
|
||||||
(const opus_int16 *)buffer,
|
(const opus_int16 *)buffer,
|
||||||
buffer_frames,
|
buffer_frames,
|
||||||
buffer2,
|
buffer2,
|
||||||
sizeof(buffer2))
|
sizeof(buffer2))
|
||||||
: opus_encode_float(enc,
|
: opus_encode_float(enc,
|
||||||
(const float *)buffer,
|
(const float *)buffer,
|
||||||
buffer_frames,
|
buffer_frames,
|
||||||
buffer2,
|
buffer2,
|
||||||
sizeof(buffer2));
|
sizeof(buffer2));
|
||||||
if (result < 0)
|
if (result < 0)
|
||||||
throw std::runtime_error("Opus encoder error");
|
throw std::runtime_error("Opus encoder error");
|
||||||
|
|
||||||
granulepos += buffer_frames;
|
granulepos += buffer_position / frame_size;
|
||||||
|
|
||||||
ogg_packet packet;
|
ogg_packet packet;
|
||||||
packet.packet = buffer2;
|
packet.packet = buffer2;
|
||||||
|
@ -217,13 +223,10 @@ OpusEncoder::DoEncode(bool eos)
|
||||||
void
|
void
|
||||||
OpusEncoder::End()
|
OpusEncoder::End()
|
||||||
{
|
{
|
||||||
Flush();
|
|
||||||
|
|
||||||
memset(buffer + buffer_position, 0,
|
memset(buffer + buffer_position, 0,
|
||||||
buffer_size - buffer_position);
|
buffer_size - buffer_position);
|
||||||
buffer_position = buffer_size;
|
|
||||||
|
|
||||||
DoEncode(true);
|
DoEncode(true);
|
||||||
|
Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -275,6 +278,13 @@ OpusEncoder::Write(const void *_data, size_t length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
OpusEncoder::GenerateHeaders(const Tag *tag)
|
||||||
|
{
|
||||||
|
GenerateHead();
|
||||||
|
GenerateTags(tag);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
OpusEncoder::GenerateHead()
|
OpusEncoder::GenerateHead()
|
||||||
{
|
{
|
||||||
|
@ -290,27 +300,65 @@ OpusEncoder::GenerateHead()
|
||||||
|
|
||||||
ogg_packet packet;
|
ogg_packet packet;
|
||||||
packet.packet = header;
|
packet.packet = header;
|
||||||
packet.bytes = 19;
|
packet.bytes = sizeof(header);
|
||||||
packet.b_o_s = true;
|
packet.b_o_s = true;
|
||||||
packet.e_o_s = false;
|
packet.e_o_s = false;
|
||||||
packet.granulepos = 0;
|
packet.granulepos = 0;
|
||||||
packet.packetno = packetno++;
|
packet.packetno = packetno++;
|
||||||
stream.PacketIn(packet);
|
stream.PacketIn(packet);
|
||||||
Flush();
|
// flush not needed because libogg autoflushes on b_o_s flag
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
OpusEncoder::GenerateTags()
|
OpusEncoder::GenerateTags(const Tag *tag)
|
||||||
{
|
{
|
||||||
const char *version = opus_get_version_string();
|
const char *version = opus_get_version_string();
|
||||||
size_t version_length = strlen(version);
|
size_t version_length = strlen(version);
|
||||||
|
|
||||||
|
// len("OpusTags") + 4 byte version length + len(version) + 4 byte tag count
|
||||||
size_t comments_size = 8 + 4 + version_length + 4;
|
size_t comments_size = 8 + 4 + version_length + 4;
|
||||||
|
uint32_t tag_count = 0;
|
||||||
|
if (tag) {
|
||||||
|
for (const auto &item: *tag) {
|
||||||
|
++tag_count;
|
||||||
|
// 4 byte length + len(tagname) + len('=') + len(value)
|
||||||
|
comments_size += 4 + strlen(tag_item_names[item.type]) + 1 + strlen(item.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsigned char *comments = (unsigned char *)xalloc(comments_size);
|
unsigned char *comments = (unsigned char *)xalloc(comments_size);
|
||||||
|
unsigned char *p = comments;
|
||||||
|
|
||||||
memcpy(comments, "OpusTags", 8);
|
memcpy(comments, "OpusTags", 8);
|
||||||
*(uint32_t *)(comments + 8) = ToLE32(version_length);
|
*(uint32_t *)(comments + 8) = ToLE32(version_length);
|
||||||
memcpy(comments + 12, version, version_length);
|
p += 12;
|
||||||
*(uint32_t *)(comments + 12 + version_length) = ToLE32(0);
|
|
||||||
|
memcpy(p, version, version_length);
|
||||||
|
p += version_length;
|
||||||
|
|
||||||
|
tag_count = ToLE32(tag_count);
|
||||||
|
memcpy(p, &tag_count, 4);
|
||||||
|
p += 4;
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
for (const auto &item: *tag) {
|
||||||
|
size_t tag_name_len = strlen(tag_item_names[item.type]);
|
||||||
|
size_t tag_val_len = strlen(item.value);
|
||||||
|
uint32_t tag_len_le = ToLE32(tag_name_len + 1 + tag_val_len);
|
||||||
|
|
||||||
|
memcpy(p, &tag_len_le, 4);
|
||||||
|
p += 4;
|
||||||
|
|
||||||
|
ToUpperASCII((char *)p, tag_item_names[item.type], tag_name_len + 1);
|
||||||
|
p += tag_name_len;
|
||||||
|
|
||||||
|
*p++ = '=';
|
||||||
|
|
||||||
|
memcpy(p, item.value, tag_val_len);
|
||||||
|
p += tag_val_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(comments + comments_size == p);
|
||||||
|
|
||||||
ogg_packet packet;
|
ogg_packet packet;
|
||||||
packet.packet = comments;
|
packet.packet = comments;
|
||||||
|
@ -325,15 +373,21 @@ OpusEncoder::GenerateTags()
|
||||||
free(comments);
|
free(comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t
|
void
|
||||||
OpusEncoder::Read(void *dest, size_t length)
|
OpusEncoder::PreTag()
|
||||||
{
|
{
|
||||||
if (packetno == 0)
|
End();
|
||||||
GenerateHead();
|
packetno = 0;
|
||||||
else if (packetno == 1)
|
granulepos = 0; // not really required, but useful to prevent wraparound
|
||||||
GenerateTags();
|
opus_encoder_ctl(enc, OPUS_RESET_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
return OggEncoder::Read(dest, length);
|
void
|
||||||
|
OpusEncoder::SendTag(const Tag &tag)
|
||||||
|
{
|
||||||
|
stream.Reinitialize(GenerateOggSerial());
|
||||||
|
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
|
||||||
|
GenerateHeaders(&tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue