aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--qxmpp.pri5
-rw-r--r--src/base/QXmppCodec.cpp243
-rw-r--r--src/base/QXmppCodec_p.h32
-rw-r--r--src/base/QXmppRtpChannel.cpp16
5 files changed, 276 insertions, 21 deletions
diff --git a/README.md b/README.md
index 64bd3702..dd50e95d 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ You can pass the following arguments to qmake:
QXMPP_LIBRARY_TYPE=staticlib to build a static version of QXmpp
QXMPP_USE_DOXYGEN=1 to build the HTML documentation
QXMPP_USE_SPEEX=1 to enable speex audio codec
+ QXMPP_USE_OPUS=1 to enable opus audio codec
QXMPP_USE_THEORA=1 to enable theora video codec
QXMPP_USE_VPX=1 to enable vpx video codec
diff --git a/qxmpp.pri b/qxmpp.pri
index ee46c6e9..b28304d4 100644
--- a/qxmpp.pri
+++ b/qxmpp.pri
@@ -38,6 +38,11 @@ android {
QXMPP_INTERNAL_LIBS += -lspeex
}
+!isEmpty(QXMPP_USE_OPUS) {
+ DEFINES += QXMPP_USE_OPUS
+ QXMPP_INTERNAL_LIBS += -lopus
+}
+
!isEmpty(QXMPP_USE_THEORA) {
DEFINES += QXMPP_USE_THEORA
QXMPP_INTERNAL_LIBS += -ltheoradec -ltheoraenc
diff --git a/src/base/QXmppCodec.cpp b/src/base/QXmppCodec.cpp
index 2a2c6c1a..3252b9e8 100644
--- a/src/base/QXmppCodec.cpp
+++ b/src/base/QXmppCodec.cpp
@@ -28,6 +28,7 @@
#include <QDataStream>
#include <QDebug>
#include <QSize>
+#include <QThread>
#include "QXmppCodec_p.h"
#include "QXmppRtpChannel.h"
@@ -38,6 +39,10 @@
#include <speex/speex.h>
#endif
+#ifdef QXMPP_USE_OPUS
+#include <opus/opus.h>
+#endif
+
#ifdef QXMPP_USE_THEORA
#include <theora/theoradec.h>
#include <theora/theoraenc.h>
@@ -60,6 +65,9 @@
#define SEG_SHIFT (4) /* Left shift for segment number. */
#define SEG_MASK (0x70) /* Segment field mask. */
+// Distance (in frames) between two key frames (video only).
+#define GOPSIZE 32
+
enum FragmentType {
NoFragment = 0,
StartFragment,
@@ -386,6 +394,149 @@ qint64 QXmppSpeexCodec::decode(QDataStream &input, QDataStream &output)
#endif
+#ifdef QXMPP_USE_OPUS
+QXmppOpusCodec::QXmppOpusCodec(int clockrate, int channels):
+ sampleRate(clockrate),
+ nChannels(channels)
+{
+ int error;
+ encoder = opus_encoder_create(clockrate, channels, OPUS_APPLICATION_VOIP, &error);
+
+ if (encoder || error == OPUS_OK) {
+ // Add some options for error correction.
+ opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1));
+ opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(20));
+ opus_encoder_ctl(encoder, OPUS_SET_DTX(1));
+ opus_encoder_ctl(encoder, OPUS_SET_PREDICTION_DISABLED(1));
+ }
+ else
+ qCritical() << "Opus encoder initialization error:" << opus_strerror(error);
+
+ // Here, clockrate is synonym of sampleRate.
+ decoder = opus_decoder_create(clockrate, channels, &error);
+
+ if (!encoder || error != OPUS_OK)
+ qCritical() << "Opus decoder initialization error:" << opus_strerror(error);
+
+ // Opus only supports fixed frame durations from 2.5ms to 60ms.
+ //
+ // NOTE: https://mf4.xiph.org/jenkins/view/opus/job/opus/ws/doc/html/group__opus__encoder.html
+ validFrameSize << 2.5e-3 << 5e-3 << 10e-3 << 20e-3 << 40e-3 << 60e-3;
+
+ // so now, calculate the equivalent number of samples to process in each
+ // frame.
+ //
+ // nSamples = t * sampleRate
+ for (int i = 0; i < validFrameSize.size(); i++)
+ validFrameSize[i] *= clockrate;
+
+ // Maxmimum number of samples for the audio buffer.
+ nSamples = validFrameSize.last();
+}
+
+QXmppOpusCodec::~QXmppOpusCodec()
+{
+ if (encoder) {
+ opus_encoder_destroy(encoder);
+ encoder = NULL;
+ }
+
+ if (decoder) {
+ opus_decoder_destroy(decoder);
+ decoder = NULL;
+ }
+}
+
+qint64 QXmppOpusCodec::encode(QDataStream &input, QDataStream &output)
+{
+ // Read an audio frame.
+ QByteArray pcm_buffer(input.device()->bytesAvailable(), 0);
+ int length = input.readRawData(pcm_buffer.data(), pcm_buffer.size());
+
+ // and append it to the sample buffer.
+ sampleBuffer.append(pcm_buffer.left(length));
+
+ // Get the maximum number of samples to encode. It must be a number
+ // accepted by the Opus encoder
+ int samples = readWindow(sampleBuffer.size());
+
+ if (samples < 1)
+ return 0;
+
+ // The encoded stream is supposed to be smaller than the raw stream, so
+ QByteArray opus_buffer(sampleBuffer.size(), 0);
+
+ length = opus_encode(encoder,
+ (opus_int16 *) sampleBuffer.constData(),
+ samples,
+ (uchar *) opus_buffer.data(),
+ opus_buffer.size());
+
+ if (length < 1)
+ qWarning() << "Opus encoding error:" << opus_strerror(length);
+ else
+ // Write the encoded stream to the output.
+ output.writeRawData(opus_buffer.constData(), length);
+
+ // Remove the frame from the sample buffer.
+ sampleBuffer.remove(0, samples * nChannels * 2);
+
+ if (length < 1)
+ return 0;
+
+ return samples;
+}
+
+qint64 QXmppOpusCodec::decode(QDataStream &input, QDataStream &output)
+{
+ QByteArray opus_buffer(input.device()->bytesAvailable(), 0);
+ int length = input.readRawData(opus_buffer.data(), opus_buffer.size());
+
+ if (length < 1)
+ return 0;
+
+ // Audio frame is nSamples at maximum, so
+ QByteArray pcm_buffer(nSamples * nChannels * 2, 0);
+
+ // The last argumment must be 1 to enable FEC, but I don't why it results
+ // in a SIGSEV.
+ int samples = opus_decode(decoder,
+ (uchar *) opus_buffer.constData(),
+ length,
+ (opus_int16 *) pcm_buffer.data(),
+ pcm_buffer.size(),
+ 0);
+
+ if (samples < 1) {
+ qWarning() << "Opus decoding error:" << opus_strerror(samples);
+
+ return 0;
+ }
+
+ // Write the audio frame to the output.
+ output.writeRawData(pcm_buffer.constData(), samples * nChannels * 2);
+
+ return samples;
+}
+
+int QXmppOpusCodec::readWindow(int bufferSize)
+{
+ // WARNING: We are expecting 2 bytes signed samples, but this is wrong since
+ // input stream can have a different sample formats.
+
+ // Get the number of frames in the buffer.
+ int samples = bufferSize / nChannels / 2;
+
+ // Find an appropiate number of samples to read, according to Opus specs.
+ for (int i = validFrameSize.size() - 1; i >= 0; i--)
+ if (validFrameSize[i] <= samples)
+ return validFrameSize[i];
+
+ return 0;
+}
+
+#endif
+
#ifdef QXMPP_USE_THEORA
class QXmppTheoraDecoderPrivate
@@ -954,7 +1105,13 @@ public:
bool QXmppVpxDecoderPrivate::decodeFrame(const QByteArray &buffer, QXmppVideoFrame *frame)
{
- if (vpx_codec_decode(&codec, (const uint8_t*)buffer.constData(), buffer.size(), NULL, 0) != VPX_CODEC_OK) {
+ // With the VPX_DL_REALTIME option, tries to decode the frame as quick as
+ // possible, if not possible discard it.
+ if (vpx_codec_decode(&codec,
+ (const uint8_t*)buffer.constData(),
+ buffer.size(),
+ NULL,
+ VPX_DL_REALTIME) != VPX_CODEC_OK) {
qWarning("Vpx packet could not be decoded: %s", vpx_codec_error_detail(&codec));
return false;
}
@@ -993,7 +1150,16 @@ bool QXmppVpxDecoderPrivate::decodeFrame(const QByteArray &buffer, QXmppVideoFra
QXmppVpxDecoder::QXmppVpxDecoder()
{
d = new QXmppVpxDecoderPrivate;
- if (vpx_codec_dec_init(&d->codec, vpx_codec_vp8_dx(), NULL, 0) != VPX_CODEC_OK) {
+ vpx_codec_flags_t flags = 0;
+
+ // Enable FEC if codec support it.
+ if (vpx_codec_get_caps(vpx_codec_vp8_dx()) & VPX_CODEC_CAP_ERROR_CONCEALMENT)
+ flags |= VPX_CODEC_USE_ERROR_CONCEALMENT;
+
+ if (vpx_codec_dec_init(&d->codec,
+ vpx_codec_vp8_dx(),
+ NULL,
+ flags) != VPX_CODEC_OK) {
qWarning("Vpx decoder could not be initialised");
}
}
@@ -1035,31 +1201,54 @@ QList<QXmppVideoFrame> QXmppVpxDecoder::handlePacket(const QXmppRtpPacket &packe
#endif
QXmppVideoFrame frame;
+ static quint16 sequence = 0;
+
+ // If the incomming packet sequence is wrong discard all packets until a
+ // complete keyframe arrives.
+ // If a partition of a keyframe is missing, discard it until a next
+ // keyframe.
+ //
+ // NOTE: https://tools.ietf.org/html/draft-ietf-payload-vp8-13#section-4.3
+ // Sections: 4.3, 4.5, 4.5.1
if (frag_type == NoFragment) {
// unfragmented packet
- if (d->decodeFrame(packet.payload.mid(1), &frame))
- frames << frame;
+ if ((packet.payload[1] & 0x1) == 0 // is key frame
+ || packet.sequence == sequence) {
+ if (d->decodeFrame(packet.payload.mid(1), &frame))
+ frames << frame;
+
+ sequence = packet.sequence + 1;
+ }
+
d->packetBuffer.resize(0);
} else {
// fragments
if (frag_type == StartFragment) {
// start fragment
- d->packetBuffer = packet.payload.mid(1);
+ if ((packet.payload[1] & 0x1) == 0 // is key frame
+ || packet.sequence == sequence) {
+ d->packetBuffer = packet.payload.mid(1);
+ sequence = packet.sequence + 1;
+ }
} else {
// continuation or end fragment
- const int packetPos = d->packetBuffer.size();
- d->packetBuffer.resize(packetPos + packetLength);
- stream.readRawData(d->packetBuffer.data() + packetPos, packetLength);
- }
+ if (packet.sequence == sequence) {
+ const int packetPos = d->packetBuffer.size();
+ d->packetBuffer.resize(packetPos + packetLength);
+ stream.readRawData(d->packetBuffer.data() + packetPos, packetLength);
+
+ if (frag_type == EndFragment) {
+ // end fragment
+ if (d->decodeFrame(d->packetBuffer, &frame)) {
+ frames << frame;
+ d->packetBuffer.resize(0);
+ }
+ }
- if (frag_type == EndFragment) {
- // end fragment
- if (d->decodeFrame(d->packetBuffer, &frame))
- frames << frame;
- d->packetBuffer.resize(0);
+ sequence++;
+ }
}
-
}
return frames;
@@ -1092,12 +1281,32 @@ void QXmppVpxEncoderPrivate::writeFragment(QDataStream &stream, FragmentType fra
stream.writeRawData(data, length);
}
-QXmppVpxEncoder::QXmppVpxEncoder()
+QXmppVpxEncoder::QXmppVpxEncoder(uint clockrate)
{
d = new QXmppVpxEncoderPrivate;
d->frameCount = 0;
d->imageBuffer = 0;
vpx_codec_enc_config_default(vpx_codec_vp8_cx(), &d->cfg, 0);
+
+ // Set the encoding threads number to use
+ int nThreads = QThread::idealThreadCount();
+
+ if (nThreads > 0)
+ d->cfg.g_threads = nThreads - 1;
+
+ // Make stream error resiliant
+ d->cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT
+ | VPX_ERROR_RESILIENT_PARTITIONS;
+
+ d->cfg.g_pass = VPX_RC_ONE_PASS;
+ d->cfg.kf_mode = VPX_KF_AUTO;
+
+ // Reduce GOP size
+ if (d->cfg.kf_max_dist > GOPSIZE)
+ d->cfg.kf_max_dist = GOPSIZE;
+
+ // Here, clockrate is synonym of bitrate.
+ d->cfg.rc_target_bitrate = clockrate / 1000;
}
QXmppVpxEncoder::~QXmppVpxEncoder()
@@ -1115,8 +1324,6 @@ bool QXmppVpxEncoder::setFormat(const QXmppVideoFormat &format)
qWarning("Vpx encoder does not support the given format");
return false;
}
-
- d->cfg.rc_target_bitrate = format.frameSize().width() * format.frameSize().height() * d->cfg.rc_target_bitrate / d->cfg.g_w / d->cfg.g_h;
d->cfg.g_w = format.frameSize().width();
d->cfg.g_h = format.frameSize().height();
if (vpx_codec_enc_init(&d->codec, vpx_codec_vp8_cx(), &d->cfg, 0) != VPX_CODEC_OK) {
diff --git a/src/base/QXmppCodec_p.h b/src/base/QXmppCodec_p.h
index 4cccdb41..2b6c0371 100644
--- a/src/base/QXmppCodec_p.h
+++ b/src/base/QXmppCodec_p.h
@@ -108,6 +108,36 @@ private:
};
#endif
+#ifdef QXMPP_USE_OPUS
+typedef struct OpusEncoder OpusEncoder;
+typedef struct OpusDecoder OpusDecoder;
+
+/// \internal
+///
+/// The QXmppOpusCodec class represent a Opus codec.
+
+class QXMPP_AUTOTEST_EXPORT QXmppOpusCodec : public QXmppCodec
+{
+public:
+ QXmppOpusCodec(int clockrate, int channels);
+ ~QXmppOpusCodec();
+
+ qint64 encode(QDataStream &input, QDataStream &output);
+ qint64 decode(QDataStream &input, QDataStream &output);
+
+private:
+ OpusEncoder *encoder;
+ OpusDecoder *decoder;
+ int sampleRate;
+ int nChannels;
+ QList<float> validFrameSize;
+ int nSamples;
+ QByteArray sampleBuffer;
+
+ int readWindow(int bufferSize);
+};
+#endif
+
/// \brief The QXmppVideoDecoder class is the base class for video decoders.
///
@@ -198,7 +228,7 @@ private:
class QXMPP_AUTOTEST_EXPORT QXmppVpxEncoder : public QXmppVideoEncoder
{
public:
- QXmppVpxEncoder();
+ QXmppVpxEncoder(uint clockrate=0);
~QXmppVpxEncoder();
bool setFormat(const QXmppVideoFormat &format);
diff --git a/src/base/QXmppRtpChannel.cpp b/src/base/QXmppRtpChannel.cpp
index 078150cd..ff478fd4 100644
--- a/src/base/QXmppRtpChannel.cpp
+++ b/src/base/QXmppRtpChannel.cpp
@@ -301,6 +301,10 @@ QXmppCodec *QXmppRtpAudioChannelPrivate::codecForPayloadType(const QXmppJinglePa
else if (payloadType.name().toLower() == "speex")
return new QXmppSpeexCodec(payloadType.clockrate());
#endif
+#ifdef QXMPP_USE_OPUS
+ else if (payloadType.name().toLower() == "opus")
+ return new QXmppOpusCodec(payloadType.clockrate(), payloadType.channels());
+#endif
return 0;
}
@@ -321,6 +325,14 @@ QXmppRtpAudioChannel::QXmppRtpAudioChannel(QObject *parent)
// set supported codecs
QXmppJinglePayloadType payload;
+#ifdef QXMPP_USE_OPUS
+ payload.setId(100); // NOTE: I don't know if this Id is ok for Opus.
+ payload.setChannels(1);
+ payload.setName("opus");
+ payload.setClockrate(8000);
+ m_outgoingPayloadTypes << payload;
+#endif
+
#ifdef QXMPP_USE_SPEEX
payload.setId(96);
payload.setChannels(1);
@@ -878,7 +890,7 @@ QXmppRtpVideoChannel::QXmppRtpVideoChannel(QObject *parent)
encoder->setFormat(d->outgoingFormat);
payload.setId(96);
payload.setName("vp8");
- payload.setClockrate(90000);
+ payload.setClockrate(256000);
payload.setParameters(encoder->parameters());
m_outgoingPayloadTypes << payload;
delete encoder;
@@ -1010,7 +1022,7 @@ void QXmppRtpVideoChannel::payloadTypesChanged()
#endif
#ifdef QXMPP_USE_VPX
else if (payload.name().toLower() == "vp8") {
- encoder = new QXmppVpxEncoder;
+ encoder = new QXmppVpxEncoder(payload.clockrate());
}
#endif
if (encoder) {