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
|
||||
* mixer
|
||||
- sndio: new mixer plugin
|
||||
* encoder
|
||||
- opus: support for sending metadata using ogg stream chaining
|
||||
* require GCC 5.0
|
||||
|
||||
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".
|
||||
</entry>
|
||||
</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>
|
||||
</tgroup>
|
||||
</informaltable>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include "config/ConfigError.hxx"
|
||||
#include "util/Alloc.hxx"
|
||||
#include "system/ByteOrder.hxx"
|
||||
#include "util/StringUtil.hxx"
|
||||
|
||||
#include <opus.h>
|
||||
#include <ogg/ogg.h>
|
||||
|
@ -55,27 +56,30 @@ class OpusEncoder final : public OggEncoder {
|
|||
ogg_int64_t granulepos = 0;
|
||||
|
||||
public:
|
||||
OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc);
|
||||
OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining);
|
||||
~OpusEncoder() override;
|
||||
|
||||
/* virtual methods from class Encoder */
|
||||
void End() 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:
|
||||
void DoEncode(bool eos);
|
||||
void WriteSilence(unsigned fill_frames);
|
||||
|
||||
void GenerateHeaders(const Tag *tag);
|
||||
void GenerateHead();
|
||||
void GenerateTags();
|
||||
void GenerateTags(const Tag *tag);
|
||||
};
|
||||
|
||||
class PreparedOpusEncoder final : public PreparedEncoder {
|
||||
opus_int32 bitrate;
|
||||
int complexity;
|
||||
int signal;
|
||||
const bool chaining;
|
||||
|
||||
public:
|
||||
PreparedOpusEncoder(const ConfigBlock &block);
|
||||
|
@ -89,6 +93,7 @@ public:
|
|||
};
|
||||
|
||||
PreparedOpusEncoder::PreparedOpusEncoder(const ConfigBlock &block)
|
||||
:chaining(block.GetBlockValue("opustags", false))
|
||||
{
|
||||
const char *value = block.GetBlockValue("bitrate", "auto");
|
||||
if (strcmp(value, "auto") == 0)
|
||||
|
@ -124,8 +129,8 @@ opus_encoder_init(const ConfigBlock &block)
|
|||
return new PreparedOpusEncoder(block);
|
||||
}
|
||||
|
||||
OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc)
|
||||
:OggEncoder(false),
|
||||
OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining)
|
||||
:OggEncoder(_chaining),
|
||||
audio_format(_audio_format),
|
||||
frame_size(_audio_format.GetFrameSize()),
|
||||
buffer_frames(_audio_format.sample_rate / 50),
|
||||
|
@ -134,6 +139,7 @@ OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc)
|
|||
enc(_enc)
|
||||
{
|
||||
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
|
||||
GenerateHeaders(nullptr);
|
||||
}
|
||||
|
||||
Encoder *
|
||||
|
@ -171,7 +177,7 @@ PreparedOpusEncoder::Open(AudioFormat &audio_format)
|
|||
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
|
||||
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal));
|
||||
|
||||
return new OpusEncoder(audio_format, enc);
|
||||
return new OpusEncoder(audio_format, enc, chaining);
|
||||
}
|
||||
|
||||
OpusEncoder::~OpusEncoder()
|
||||
|
@ -183,7 +189,7 @@ OpusEncoder::~OpusEncoder()
|
|||
void
|
||||
OpusEncoder::DoEncode(bool eos)
|
||||
{
|
||||
assert(buffer_position == buffer_size);
|
||||
assert(buffer_position == buffer_size || eos);
|
||||
|
||||
opus_int32 result =
|
||||
audio_format.format == SampleFormat::S16
|
||||
|
@ -200,7 +206,7 @@ OpusEncoder::DoEncode(bool eos)
|
|||
if (result < 0)
|
||||
throw std::runtime_error("Opus encoder error");
|
||||
|
||||
granulepos += buffer_frames;
|
||||
granulepos += buffer_position / frame_size;
|
||||
|
||||
ogg_packet packet;
|
||||
packet.packet = buffer2;
|
||||
|
@ -217,13 +223,10 @@ OpusEncoder::DoEncode(bool eos)
|
|||
void
|
||||
OpusEncoder::End()
|
||||
{
|
||||
Flush();
|
||||
|
||||
memset(buffer + buffer_position, 0,
|
||||
buffer_size - buffer_position);
|
||||
buffer_position = buffer_size;
|
||||
|
||||
DoEncode(true);
|
||||
Flush();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -275,6 +278,13 @@ OpusEncoder::Write(const void *_data, size_t length)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
OpusEncoder::GenerateHeaders(const Tag *tag)
|
||||
{
|
||||
GenerateHead();
|
||||
GenerateTags(tag);
|
||||
}
|
||||
|
||||
void
|
||||
OpusEncoder::GenerateHead()
|
||||
{
|
||||
|
@ -290,27 +300,65 @@ OpusEncoder::GenerateHead()
|
|||
|
||||
ogg_packet packet;
|
||||
packet.packet = header;
|
||||
packet.bytes = 19;
|
||||
packet.bytes = sizeof(header);
|
||||
packet.b_o_s = true;
|
||||
packet.e_o_s = false;
|
||||
packet.granulepos = 0;
|
||||
packet.packetno = packetno++;
|
||||
stream.PacketIn(packet);
|
||||
Flush();
|
||||
// flush not needed because libogg autoflushes on b_o_s flag
|
||||
}
|
||||
|
||||
void
|
||||
OpusEncoder::GenerateTags()
|
||||
OpusEncoder::GenerateTags(const Tag *tag)
|
||||
{
|
||||
const char *version = opus_get_version_string();
|
||||
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;
|
||||
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 *p = comments;
|
||||
|
||||
memcpy(comments, "OpusTags", 8);
|
||||
*(uint32_t *)(comments + 8) = ToLE32(version_length);
|
||||
memcpy(comments + 12, version, version_length);
|
||||
*(uint32_t *)(comments + 12 + version_length) = ToLE32(0);
|
||||
p += 12;
|
||||
|
||||
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;
|
||||
packet.packet = comments;
|
||||
|
@ -325,15 +373,21 @@ OpusEncoder::GenerateTags()
|
|||
free(comments);
|
||||
}
|
||||
|
||||
size_t
|
||||
OpusEncoder::Read(void *dest, size_t length)
|
||||
void
|
||||
OpusEncoder::PreTag()
|
||||
{
|
||||
if (packetno == 0)
|
||||
GenerateHead();
|
||||
else if (packetno == 1)
|
||||
GenerateTags();
|
||||
End();
|
||||
packetno = 0;
|
||||
granulepos = 0; // not really required, but useful to prevent wraparound
|
||||
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