diff --git a/src/core/UBApplication.cpp b/src/core/UBApplication.cpp
index e364027d..5e205fea 100644
--- a/src/core/UBApplication.cpp
+++ b/src/core/UBApplication.cpp
@@ -520,10 +520,8 @@ void UBApplication::decorateActionMenu(QAction* action)
menu->addSeparator();
-#ifndef Q_OS_LINUX // No Podcast on Linux yet
menu->addAction(mainWindow->actionPodcast);
mainWindow->actionPodcast->setText(tr("Podcast"));
-#endif
menu->addSeparator();
menu->addAction(mainWindow->actionQuit);
diff --git a/src/podcast/UBPodcastController.cpp b/src/podcast/UBPodcastController.cpp
index 598b5135..efe22027 100644
--- a/src/podcast/UBPodcastController.cpp
+++ b/src/podcast/UBPodcastController.cpp
@@ -64,6 +64,9 @@
#elif defined(Q_OS_OSX)
#include "quicktime/UBQuickTimeVideoEncoder.h"
#include "quicktime/UBAudioQueueRecorder.h"
+#elif defined(Q_OS_LINUX)
+ #include "ffmpeg/UBFFmpegVideoEncoder.h"
+ #include "ffmpeg/UBMicrophoneInput.h"
#endif
#include "core/memcheck.h"
@@ -309,6 +312,8 @@ void UBPodcastController::start()
mVideoEncoder = new UBWindowsMediaVideoEncoder(this); //deleted on stop
#elif defined(Q_OS_OSX)
mVideoEncoder = new UBQuickTimeVideoEncoder(this); //deleted on stop
+#elif defined(Q_OS_LINUX)
+ mVideoEncoder = new UBFFmpegVideoEncoder(this);
#endif
if (mVideoEncoder)
@@ -804,6 +809,8 @@ QStringList UBPodcastController::audioRecordingDevices()
devices = UBWaveRecorder::waveInDevices();
#elif defined(Q_OS_OSX)
devices = UBAudioQueueRecorder::waveInDevices();
+#elif defined(Q_OS_LINUX)
+ devices = UBMicrophoneInput::availableDevicesNames();
#endif
return devices;
diff --git a/src/podcast/ffmpeg/UBFFmpegVideoEncoder.cpp b/src/podcast/ffmpeg/UBFFmpegVideoEncoder.cpp
new file mode 100644
index 00000000..fa6bc0dd
--- /dev/null
+++ b/src/podcast/ffmpeg/UBFFmpegVideoEncoder.cpp
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2015-2016 Département de l'Instruction Publique (DIP-SEM)
+ *
+ * This file is part of OpenBoard.
+ *
+ * OpenBoard 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, version 3 of the License,
+ * with a specific linking exception for the OpenSSL project's
+ * "OpenSSL" library (or with modified versions of it that use the
+ * same license as the "OpenSSL" library).
+ *
+ * OpenBoard 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenBoard. If not, see .
+ */
+
+#include "UBFFmpegVideoEncoder.h"
+
+//-------------------------------------------------------------------------
+// Utility functions
+//-------------------------------------------------------------------------
+
+QString avErrorToQString(int errnum)
+{
+ char error[AV_ERROR_MAX_STRING_SIZE];
+ av_make_error_string(error, AV_ERROR_MAX_STRING_SIZE, errnum);
+
+ return QString(error);
+}
+
+/**
+ * @brief Write a given frame to the audio stream or, if a null frame is passed, flush the stream.
+ *
+ * @param frame An AVFrame to be written to the stream, or NULL to flush the stream
+ * @param packet A (reusable) packet, used to temporarily store frame data
+ * @param stream The stream to write to
+ * @param outputFormatContext The output format context
+ */
+void writeFrame(AVFrame *frame, AVPacket *packet, AVStream *stream, AVFormatContext *outputFormatContext)
+{
+ int gotOutput, ret;
+
+ av_init_packet(packet);
+
+ do {
+ if (stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
+ ret = avcodec_encode_audio2(stream->codec, packet, frame, &gotOutput);
+ else
+ ret = avcodec_encode_video2(stream->codec, packet, frame, &gotOutput);
+
+ if (ret < 0)
+ qWarning() << "Couldn't encode audio frame: " << avErrorToQString(ret);
+
+ else if (gotOutput) {
+ AVRational codecTimebase = stream->codec->time_base;
+ AVRational streamVideoTimebase = stream->time_base;
+
+ av_packet_rescale_ts(packet, codecTimebase, streamVideoTimebase);
+ packet->stream_index = stream->index;
+
+ av_interleaved_write_frame(outputFormatContext, packet);
+ av_packet_unref(packet);
+ }
+
+ } while (gotOutput && !frame);
+}
+
+void flushStream(AVPacket *packet, AVStream *stream, AVFormatContext *outputFormatContext)
+{
+ writeFrame(NULL, packet, stream, outputFormatContext);
+}
+
+//-------------------------------------------------------------------------
+// UBFFmpegVideoEncoder
+//-------------------------------------------------------------------------
+
+UBFFmpegVideoEncoder::UBFFmpegVideoEncoder(QObject* parent)
+ : UBAbstractVideoEncoder(parent)
+ , mOutputFormatContext(NULL)
+ , mSwsContext(NULL)
+ , mShouldRecordAudio(true)
+ , mAudioInput(NULL)
+ , mSwrContext(NULL)
+ , mAudioOutBuffer(NULL)
+ , mAudioSampleRate(44100)
+ , mAudioFrameCount(0)
+{
+
+ mVideoTimebase = 100 * framesPerSecond();
+
+ mVideoEncoderThread = new QThread;
+ mVideoWorker = new UBFFmpegVideoEncoderWorker(this);
+ mVideoWorker->moveToThread(mVideoEncoderThread);
+
+ connect(mVideoWorker, SIGNAL(error(QString)),
+ this, SLOT(setLastErrorMessage(QString)));
+
+ connect(mVideoEncoderThread, SIGNAL(started()),
+ mVideoWorker, SLOT(runEncoding()));
+
+ connect(mVideoWorker, SIGNAL(encodingFinished()),
+ mVideoEncoderThread, SLOT(quit()));
+
+ connect(mVideoEncoderThread, SIGNAL(finished()),
+ this, SLOT(finishEncoding()));
+}
+
+UBFFmpegVideoEncoder::~UBFFmpegVideoEncoder()
+{
+ if (mVideoWorker)
+ delete mVideoWorker;
+
+ if (mVideoEncoderThread)
+ delete mVideoEncoderThread;
+
+ if (mAudioInput)
+ delete mAudioInput;
+}
+
+void UBFFmpegVideoEncoder::setLastErrorMessage(const QString& pMessage)
+{
+ qWarning() << "FFmpeg video encoder:" << pMessage;
+ mLastErrorMessage = pMessage;
+}
+
+
+bool UBFFmpegVideoEncoder::start()
+{
+ bool initialized = init();
+
+ if (initialized) {
+ mVideoEncoderThread->start();
+ if (mShouldRecordAudio)
+ mAudioInput->start();
+ }
+
+ return initialized;
+}
+
+bool UBFFmpegVideoEncoder::stop()
+{
+ qDebug() << "Video encoder: stop requested";
+
+ mVideoWorker->stopEncoding();
+
+ if (mShouldRecordAudio)
+ mAudioInput->stop();
+
+ return true;
+}
+
+bool UBFFmpegVideoEncoder::init()
+{
+ av_register_all();
+ avcodec_register_all();
+
+ AVDictionary * options = NULL;
+ int ret;
+
+ // Output format and context
+ // --------------------------------------
+ if (avformat_alloc_output_context2(&mOutputFormatContext, NULL,
+ "mp4", NULL) < 0)
+ {
+ setLastErrorMessage("Couldn't allocate video format context");
+ return false;
+ }
+
+ // The default codecs for mp4 are h264 and aac, we use those
+
+
+ // Video codec and context
+ // -------------------------------------
+ mVideoStream = avformat_new_stream(mOutputFormatContext, 0);
+
+ AVCodec * videoCodec = avcodec_find_encoder(mOutputFormatContext->oformat->video_codec);
+ if (!videoCodec) {
+ setLastErrorMessage("Video codec not found");
+ return false;
+ }
+
+ AVCodecContext* c = avcodec_alloc_context3(videoCodec);
+
+ c->bit_rate = videoBitsPerSecond();
+ c->width = videoSize().width();
+ c->height = videoSize().height();
+ c->time_base = {1, mVideoTimebase};
+ c->gop_size = 10;
+ c->max_b_frames = 0;
+ c->pix_fmt = AV_PIX_FMT_YUV420P;
+
+ if (mOutputFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
+ c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ /*
+ * Supported pixel formats for h264 are:
+ * AV_PIX_FMT_YUV420P
+ * AV_PIX_FMT_YUV422P
+ * AV_PIX_FMT_YUV444P
+ * AV_PIX_FMT_YUVJ420P
+ */
+
+ av_dict_set(&options, "preset", "slow", 0);
+ av_dict_set(&options, "crf", "20", 0);
+
+ ret = avcodec_open2(c, videoCodec, &options);
+
+ if (ret < 0) {
+ setLastErrorMessage(QString("Couldn't open video codec: ") + avErrorToQString(ret));
+ return false;
+ }
+
+ mVideoStream->codec = c;
+
+ // Source images are RGB32, and should be converted to YUV for h264 video
+ mSwsContext = sws_getCachedContext(mSwsContext,
+ c->width, c->height, AV_PIX_FMT_RGB32,
+ c->width, c->height, c->pix_fmt,
+ SWS_BICUBIC, 0, 0, 0);
+
+ // Audio codec and context
+ // -------------------------------------
+ if (mShouldRecordAudio) {
+
+ // Microphone input
+
+ mAudioInput = new UBMicrophoneInput();
+
+ connect(mAudioInput, SIGNAL(audioLevelChanged(quint8)),
+ this, SIGNAL(audioLevelChanged(quint8)));
+
+ connect(mAudioInput, SIGNAL(dataAvailable(QByteArray)),
+ this, SLOT(onAudioAvailable(QByteArray)));
+
+ if (!mAudioInput->init()) {
+ setLastErrorMessage("Couldn't initialize audio input");
+ return false;
+ }
+
+ int inChannelCount = mAudioInput->channelCount();
+ int inSampleRate = mAudioInput->sampleRate();
+
+ // Codec
+
+ AVCodec * audioCodec = avcodec_find_encoder(mOutputFormatContext->oformat->audio_codec);
+
+ if (!audioCodec) {
+ setLastErrorMessage("Audio codec not found");
+ return false;
+ }
+
+ mAudioStream = avformat_new_stream(mOutputFormatContext, audioCodec);
+ mAudioStream->id = mOutputFormatContext->nb_streams-1;
+
+ c = mAudioStream->codec;
+
+ c->bit_rate = 96000;
+ c->sample_fmt = audioCodec->sample_fmts[0]; // FLTP by default for AAC
+ c->sample_rate = mAudioSampleRate;
+ c->channels = 2;
+ c->channel_layout = av_get_default_channel_layout(c->channels);
+ c->profile = FF_PROFILE_AAC_MAIN;
+ c->time_base = {1, mAudioSampleRate};
+
+ if (mOutputFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
+ c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ ret = avcodec_open2(c, audioCodec, NULL);
+
+ if (ret < 0) {
+ setLastErrorMessage(QString("Couldn't open audio codec: ") + avErrorToQString(ret));
+ return false;
+ }
+
+ // The input (raw sound from the microphone) may not match the codec's sampling rate,
+ // sample format or number of channels; we use libswresample to convert and resample it
+ mSwrContext = swr_alloc();
+ if (!mSwrContext) {
+ setLastErrorMessage("Could not allocate resampler context");
+ return false;
+ }
+
+ av_opt_set_int(mSwrContext, "in_channel_count", inChannelCount, 0);
+ av_opt_set_int(mSwrContext, "in_sample_rate", inSampleRate, 0);
+ av_opt_set_sample_fmt(mSwrContext, "in_sample_fmt", (AVSampleFormat)mAudioInput->sampleFormat(), 0);
+ av_opt_set_int(mSwrContext, "out_channel_count", c->channels, 0);
+ av_opt_set_int(mSwrContext, "out_sample_rate", c->sample_rate, 0);
+ av_opt_set_sample_fmt(mSwrContext, "out_sample_fmt", c->sample_fmt, 0);
+
+ ret = swr_init(mSwrContext);
+ if (ret < 0) {
+ setLastErrorMessage(QString("Couldn't initialize the resampling context: ") + avErrorToQString(ret));
+ return false;
+ }
+
+ // Buffer for resampled/converted audio
+ mAudioOutBuffer = av_audio_fifo_alloc(c->sample_fmt, c->channels, c->frame_size);
+ }
+
+
+ // Open the output file
+ ret = avio_open(&(mOutputFormatContext->pb), videoFileName().toStdString().c_str(), AVIO_FLAG_WRITE);
+
+ if (ret < 0) {
+ setLastErrorMessage(QString("Couldn't open video file for writing: ") + avErrorToQString(ret));
+ return false;
+ }
+
+ // Write stream header
+ ret = avformat_write_header(mOutputFormatContext, NULL);
+
+ if (ret < 0) {
+ setLastErrorMessage(QString("Couldn't write header to file: ") + avErrorToQString(ret));
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * This function should be called every time a new "screenshot" is ready.
+ * The image is converted to the right format and sent to the encoder.
+ */
+void UBFFmpegVideoEncoder::newPixmap(const QImage &pImage, long timestamp)
+{
+ if (!mVideoWorker->isRunning()) {
+ qDebug() << "Encoder worker thread not running. Queuing frame.";
+ mPendingFrames.enqueue({pImage, timestamp});
+ }
+
+ else {
+ // First send any queued frames, then the latest one
+ while (!mPendingFrames.isEmpty()) {
+ AVFrame* avFrame = convertImageFrame(mPendingFrames.dequeue());
+ if (avFrame)
+ mVideoWorker->queueVideoFrame(avFrame);
+ }
+
+ // note: if converting the frame turns out to be too slow to do here, it
+ // can always be done from the worker thread (in that case,
+ // the worker's queue would contain ImageFrames rather than AVFrames)
+
+ AVFrame* avFrame = convertImageFrame({pImage, timestamp});
+ if (avFrame)
+ mVideoWorker->queueVideoFrame(avFrame);
+
+ // signal the worker that frames are available
+ mVideoWorker->mWaitCondition.wakeAll();
+ }
+}
+
+/**
+ * Convert a frame consisting of a QImage and timestamp to an AVFrame
+ * with the right pixel format and PTS
+ */
+AVFrame* UBFFmpegVideoEncoder::convertImageFrame(ImageFrame frame)
+{
+ AVFrame* avFrame = av_frame_alloc();
+
+ avFrame->format = mVideoStream->codec->pix_fmt;
+ avFrame->width = mVideoStream->codec->width;
+ avFrame->height = mVideoStream->codec->height;
+ avFrame->pts = mVideoTimebase * frame.timestamp / 1000;
+
+ const uchar * rgbImage = frame.image.bits();
+
+ const int in_linesize[1] = { frame.image.bytesPerLine() };
+
+ // Allocate the output image
+ if (av_image_alloc(avFrame->data, avFrame->linesize, mVideoStream->codec->width,
+ mVideoStream->codec->height, mVideoStream->codec->pix_fmt, 32) < 0)
+ {
+ qWarning() << "Couldn't allocate image";
+ return NULL;
+ }
+
+ sws_scale(mSwsContext,
+ (const uint8_t* const*)&rgbImage,
+ in_linesize,
+ 0,
+ mVideoStream->codec->height,
+ avFrame->data,
+ avFrame->linesize);
+
+ return avFrame;
+}
+
+void UBFFmpegVideoEncoder::onAudioAvailable(QByteArray data)
+{
+ if (!data.isEmpty())
+ processAudio(data);
+}
+
+/**
+* Resample and convert audio to match the encoder's settings and queue the
+* output. If enough output data is available, it is packaged into AVFrames and
+* sent to the encoder thread.
+*/
+void UBFFmpegVideoEncoder::processAudio(QByteArray &data)
+{
+ int ret;
+ AVCodecContext* codecContext = mAudioStream->codec;
+
+ const char * inSamples = data.constData();
+
+ // The number of samples (per channel) in the input
+ int inSamplesCount = data.size() / ((mAudioInput->sampleSize() / 8) * mAudioInput->channelCount());
+
+ // The number of samples we will get after conversion
+ int outSamplesCount = swr_get_out_samples(mSwrContext, inSamplesCount);
+
+ // Allocate output samples
+ uint8_t ** outSamples = NULL;
+ int outSamplesLineSize;
+
+ ret = av_samples_alloc_array_and_samples(&outSamples, &outSamplesLineSize,
+ codecContext->channels, outSamplesCount,
+ codecContext->sample_fmt, 0);
+ if (ret < 0) {
+ qWarning() << "Could not allocate audio samples" << avErrorToQString(ret);
+ return;
+ }
+
+ // Convert to destination format
+ ret = swr_convert(mSwrContext,
+ outSamples, outSamplesCount,
+ (const uint8_t **)&inSamples, inSamplesCount);
+ if (ret < 0) {
+ qWarning() << "Error converting audio samples: " << avErrorToQString(ret);
+ return;
+ }
+
+ // Append the converted samples to the out buffer.
+ ret = av_audio_fifo_write(mAudioOutBuffer, (void**)outSamples, outSamplesCount);
+ if (ret < 0) {
+ qWarning() << "Could not write to FIFO queue: " << avErrorToQString(ret);
+ return;
+ }
+
+ // Keep the data queued until next call if the encoder thread isn't running
+ if (!mVideoWorker->isRunning())
+ return;
+
+ bool framesAdded = false;
+ while (av_audio_fifo_size(mAudioOutBuffer) > codecContext->frame_size) {
+ AVFrame * avFrame = av_frame_alloc();
+ avFrame->nb_samples = codecContext->frame_size;
+ avFrame->channel_layout = codecContext->channel_layout;
+ avFrame->format = codecContext->sample_fmt;
+ avFrame->sample_rate = codecContext->sample_rate;
+ avFrame->pts = mAudioFrameCount;
+
+ ret = av_frame_get_buffer(avFrame, 0);
+ if (ret < 0) {
+ qWarning() << "Couldn't allocate frame: " << avErrorToQString(ret);
+ break;
+ }
+
+ ret = av_audio_fifo_read(mAudioOutBuffer, (void**)avFrame->data, codecContext->frame_size);
+ if (ret < 0)
+ qWarning() << "Could not read from FIFO queue: " << avErrorToQString(ret);
+
+ else {
+ mAudioFrameCount += codecContext->frame_size;
+
+ mVideoWorker->queueAudioFrame(avFrame);
+ framesAdded = true;
+ }
+ }
+
+ if (framesAdded)
+ mVideoWorker->mWaitCondition.wakeAll();
+}
+
+void UBFFmpegVideoEncoder::finishEncoding()
+{
+ qDebug() << "VideoEncoder::finishEncoding called";
+
+ flushStream(mVideoWorker->mVideoPacket, mVideoStream, mOutputFormatContext);
+
+ if (mShouldRecordAudio)
+ flushStream(mVideoWorker->mAudioPacket, mAudioStream, mOutputFormatContext);
+
+ av_write_trailer(mOutputFormatContext);
+ avio_close(mOutputFormatContext->pb);
+
+ avcodec_close(mVideoStream->codec);
+ sws_freeContext(mSwsContext);
+
+ if (mShouldRecordAudio) {
+ avcodec_close(mAudioStream->codec);
+ swr_free(&mSwrContext);
+ }
+
+ avformat_free_context(mOutputFormatContext);
+
+ emit encodingFinished(true);
+}
+
+
+//-------------------------------------------------------------------------
+// Worker
+//-------------------------------------------------------------------------
+
+UBFFmpegVideoEncoderWorker::UBFFmpegVideoEncoderWorker(UBFFmpegVideoEncoder* controller)
+ : mController(controller)
+{
+ mStopRequested = false;
+ mIsRunning = false;
+ mVideoPacket = new AVPacket();
+ mAudioPacket = new AVPacket();
+}
+
+UBFFmpegVideoEncoderWorker::~UBFFmpegVideoEncoderWorker()
+{
+ if (mVideoPacket)
+ delete mVideoPacket;
+
+ if (mAudioPacket)
+ delete mAudioPacket;
+}
+
+void UBFFmpegVideoEncoderWorker::stopEncoding()
+{
+ qDebug() << "Video worker: stop requested";
+ mStopRequested = true;
+ mWaitCondition.wakeAll();
+}
+
+void UBFFmpegVideoEncoderWorker::queueVideoFrame(AVFrame* frame)
+{
+ if (frame) {
+ mFrameQueueMutex.lock();
+ mImageQueue.enqueue(frame);
+ mFrameQueueMutex.unlock();
+ }
+}
+
+void UBFFmpegVideoEncoderWorker::queueAudioFrame(AVFrame* frame)
+{
+ if (frame) {
+ mFrameQueueMutex.lock();
+ mAudioQueue.enqueue(frame);
+ mFrameQueueMutex.unlock();
+ }
+}
+
+/**
+ * The main encoding function. Takes the queued frames and
+ * writes them to the video and audio streams
+ */
+void UBFFmpegVideoEncoderWorker::runEncoding()
+{
+ mIsRunning = true;
+
+ while (!mStopRequested) {
+ mFrameQueueMutex.lock();
+ mWaitCondition.wait(&mFrameQueueMutex);
+
+ while (!mImageQueue.isEmpty()) {
+ writeLatestVideoFrame();
+ }
+
+ while (!mAudioQueue.isEmpty()) {
+ writeLatestAudioFrame();
+ }
+
+ mFrameQueueMutex.unlock();
+ }
+
+ emit encodingFinished();
+}
+
+void UBFFmpegVideoEncoderWorker::writeLatestVideoFrame()
+{
+ AVFrame* frame = mImageQueue.dequeue();
+ writeFrame(frame, mVideoPacket, mController->mVideoStream, mController->mOutputFormatContext);
+ av_frame_free(&frame);
+}
+
+void UBFFmpegVideoEncoderWorker::writeLatestAudioFrame()
+{
+ AVFrame *frame = mAudioQueue.dequeue();
+ writeFrame(frame, mAudioPacket, mController->mAudioStream, mController->mOutputFormatContext);
+ av_frame_free(&frame);
+}
diff --git a/src/podcast/ffmpeg/UBFFmpegVideoEncoder.h b/src/podcast/ffmpeg/UBFFmpegVideoEncoder.h
new file mode 100644
index 00000000..eed8c16b
--- /dev/null
+++ b/src/podcast/ffmpeg/UBFFmpegVideoEncoder.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015-2016 Département de l'Instruction Publique (DIP-SEM)
+ *
+ * This file is part of OpenBoard.
+ *
+ * OpenBoard 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, version 3 of the License,
+ * with a specific linking exception for the OpenSSL project's
+ * "OpenSSL" library (or with modified versions of it that use the
+ * same license as the "OpenSSL" library).
+ *
+ * OpenBoard 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenBoard. If not, see .
+ */
+
+#ifndef UBFFMPEGVIDEOENCODER_H
+#define UBFFMPEGVIDEOENCODER_H
+
+extern "C" {
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+}
+
+#include
+
+#include
+#include
+
+#include "podcast/UBAbstractVideoEncoder.h"
+#include "podcast/ffmpeg/UBMicrophoneInput.h"
+
+class UBFFmpegVideoEncoderWorker;
+class UBPodcastController;
+
+/**
+ * This class provides an interface between the podcast controller and the ffmpeg
+ * back-end.
+ * It includes all the necessary objects and methods to record video (muxer, audio and
+ * video streams and encoders, etc) from inputs consisting of raw PCM audio and raw RGBA
+ * images.
+ *
+ * A worker thread is used to encode and write the audio and video on-the-fly.
+ */
+
+class UBFFmpegVideoEncoder : public UBAbstractVideoEncoder
+{
+ Q_OBJECT
+
+ friend class UBFFmpegVideoEncoderWorker;
+
+public:
+
+ UBFFmpegVideoEncoder(QObject* parent = NULL);
+ virtual ~UBFFmpegVideoEncoder();
+
+ bool start();
+ bool stop();
+
+ void newPixmap(const QImage& pImage, long timestamp);
+
+ QString videoFileExtension() const { return "mp4"; }
+
+ QString lastErrorMessage() { return mLastErrorMessage; }
+
+ void setRecordAudio(bool pRecordAudio) { mShouldRecordAudio = pRecordAudio; }
+
+signals:
+
+ void encodingFinished(bool ok);
+
+private slots:
+
+ void setLastErrorMessage(const QString& pMessage);
+ void onAudioAvailable(QByteArray data);
+ void finishEncoding();
+
+private:
+
+ struct ImageFrame
+ {
+ QImage image;
+ long timestamp; // unit: ms
+ };
+
+ AVFrame* convertImageFrame(ImageFrame frame);
+ AVFrame* convertAudio(QByteArray data);
+ void processAudio(QByteArray& data);
+ bool init();
+
+ QString mLastErrorMessage;
+
+ QThread* mVideoEncoderThread;
+ UBFFmpegVideoEncoderWorker* mVideoWorker;
+
+ // Muxer
+ // ------------------------------------------
+ AVFormatContext* mOutputFormatContext;
+ AVStream* mVideoStream;
+ AVStream* mAudioStream;
+
+ // Video
+ // ------------------------------------------
+ QQueue mPendingFrames;
+ struct SwsContext * mSwsContext;
+
+ int mVideoTimebase;
+
+ // Audio
+ // ------------------------------------------
+ bool mShouldRecordAudio;
+
+ UBMicrophoneInput * mAudioInput;
+ struct SwrContext * mSwrContext;
+ /// Queue for audio that has been rescaled/converted but not encoded yet
+ AVAudioFifo *mAudioOutBuffer;
+
+ /// Sample rate for encoded audio
+ int mAudioSampleRate;
+ /// Total audio frames sent to encoder
+ int mAudioFrameCount;
+};
+
+
+class UBFFmpegVideoEncoderWorker : public QObject
+{
+ Q_OBJECT
+
+ friend class UBFFmpegVideoEncoder;
+
+public:
+ UBFFmpegVideoEncoderWorker(UBFFmpegVideoEncoder* controller);
+ ~UBFFmpegVideoEncoderWorker();
+
+ bool isRunning() { return mIsRunning; }
+
+ void queueVideoFrame(AVFrame* frame);
+ void queueAudioFrame(AVFrame* frame);
+
+public slots:
+ void runEncoding();
+ void stopEncoding();
+
+signals:
+ void encodingFinished();
+ void error(QString message);
+
+private:
+ void writeLatestVideoFrame();
+ void writeLatestAudioFrame();
+
+ UBFFmpegVideoEncoder* mController;
+
+ // std::atomic is C++11. This won't work with msvc2010, so a
+ // newer compiler must be used if this class is to be used on Windows
+ std::atomic mStopRequested;
+ std::atomic mIsRunning;
+
+ QQueue mImageQueue;
+ QQueue mAudioQueue;
+
+ QMutex mFrameQueueMutex;
+ QWaitCondition mWaitCondition;
+
+ AVPacket* mVideoPacket;
+ AVPacket* mAudioPacket;
+};
+
+#endif // UBFFMPEGVIDEOENCODER_H
diff --git a/src/podcast/ffmpeg/UBMicrophoneInput.cpp b/src/podcast/ffmpeg/UBMicrophoneInput.cpp
new file mode 100644
index 00000000..ad17dd49
--- /dev/null
+++ b/src/podcast/ffmpeg/UBMicrophoneInput.cpp
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2015-2016 Département de l'Instruction Publique (DIP-SEM)
+ *
+ * This file is part of OpenBoard.
+ *
+ * OpenBoard 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, version 3 of the License,
+ * with a specific linking exception for the OpenSSL project's
+ * "OpenSSL" library (or with modified versions of it that use the
+ * same license as the "OpenSSL" library).
+ *
+ * OpenBoard 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenBoard. If not, see .
+ */
+
+#include "UBMicrophoneInput.h"
+
+UBMicrophoneInput::UBMicrophoneInput()
+ : mAudioInput(NULL)
+ , mIODevice(NULL)
+ , mSeekPos(0)
+{
+}
+
+UBMicrophoneInput::~UBMicrophoneInput()
+{
+ if (mAudioInput)
+ delete mAudioInput;
+}
+
+bool UBMicrophoneInput::init()
+{
+ if (mAudioDeviceInfo.isNull()) {
+ qWarning("No audio input device selected; using default");
+ mAudioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
+ }
+
+ mAudioFormat = mAudioDeviceInfo.preferredFormat();
+ mAudioInput = new QAudioInput(mAudioDeviceInfo, mAudioFormat, NULL);
+
+ connect(mAudioInput, SIGNAL(stateChanged(QAudio::State)),
+ this, SLOT(onAudioInputStateChanged(QAudio::State)));
+
+
+ qDebug() << "Input device name: " << mAudioDeviceInfo.deviceName();
+ qDebug() << "Input sample format: " << mAudioFormat.sampleSize() << "bit"
+ << mAudioFormat.sampleType() << "at" << mAudioFormat.sampleRate() << "Hz"
+ << "; codec: " << mAudioFormat.codec();
+
+ return true;
+}
+
+void UBMicrophoneInput::start()
+{
+ mIODevice = mAudioInput->start();
+
+ connect(mIODevice, SIGNAL(readyRead()),
+ this, SLOT(onDataReady()));
+
+ if (mAudioInput->error() == QAudio::OpenError)
+ qWarning() << "Error opening audio input";
+}
+
+void UBMicrophoneInput::stop()
+{
+ mAudioInput->stop();
+}
+
+QStringList UBMicrophoneInput::availableDevicesNames()
+{
+ QStringList names;
+ QList devices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput);
+
+ foreach (QAudioDeviceInfo device, devices) {
+ names.push_back(device.deviceName());
+ }
+
+ return names;
+}
+
+void UBMicrophoneInput::setInputDevice(QString name)
+{
+ if (name.isEmpty()) {
+ mAudioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
+ return;
+ }
+
+ QList devices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput);
+ bool found = false;
+
+ foreach (QAudioDeviceInfo device, devices) {
+ if (device.deviceName() == name) {
+ mAudioDeviceInfo = device;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ qWarning() << "Audio input device not found; using default instead";
+ mAudioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
+ }
+
+}
+
+int UBMicrophoneInput::channelCount()
+{
+ return mAudioFormat.channelCount();
+}
+
+int UBMicrophoneInput::sampleRate()
+{
+ return mAudioFormat.sampleRate();
+}
+
+/* Return the sample size in bits */
+int UBMicrophoneInput::sampleSize()
+{
+ return mAudioFormat.sampleSize();
+}
+
+/** Return the sample format in FFMpeg style (AVSampleFormat enum) */
+int UBMicrophoneInput::sampleFormat()
+{
+ enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8,
+ AV_SAMPLE_FMT_S16,
+ AV_SAMPLE_FMT_S32,
+ AV_SAMPLE_FMT_FLT,
+ AV_SAMPLE_FMT_DBL,
+ AV_SAMPLE_FMT_U8P,
+ AV_SAMPLE_FMT_S16P,
+ AV_SAMPLE_FMT_S32P,
+ AV_SAMPLE_FMT_FLTP,
+ AV_SAMPLE_FMT_DBLP,
+ AV_SAMPLE_FMT_NB
+ };
+
+ int sampleSize = mAudioFormat.sampleSize();
+ QAudioFormat::SampleType sampleType = mAudioFormat.sampleType();
+
+ switch (sampleType) {
+ case QAudioFormat::Unknown:
+ return AV_SAMPLE_FMT_NONE;
+
+ case QAudioFormat::SignedInt:
+ if (sampleSize == 16)
+ return AV_SAMPLE_FMT_S16;
+ if (sampleSize == 32)
+ return AV_SAMPLE_FMT_S32;
+ break;
+
+ case QAudioFormat::UnSignedInt:
+ if (sampleSize == 8)
+ return AV_SAMPLE_FMT_U8;
+ break;
+
+ case QAudioFormat::Float:
+ return AV_SAMPLE_FMT_FLT;
+
+ default:
+ return AV_SAMPLE_FMT_NONE;
+ }
+
+ return AV_SAMPLE_FMT_NONE;
+}
+
+QString UBMicrophoneInput::codec()
+{
+ return mAudioFormat.codec();
+}
+
+static qint64 uSecsElapsed = 0;
+void UBMicrophoneInput::onDataReady()
+{
+ int numBytes = mAudioInput->bytesReady();
+
+ uSecsElapsed += mAudioFormat.durationForBytes(numBytes);
+
+ // Only emit data every 100ms
+ if (uSecsElapsed > 100000) {
+ uSecsElapsed = 0;
+ QByteArray data = mIODevice->read(numBytes);
+
+ quint8 level = audioLevel(data);
+ if (level != mLastAudioLevel) {
+ mLastAudioLevel = level;
+ emit audioLevelChanged(level);
+ }
+
+ emit dataAvailable(data);
+ }
+}
+
+void UBMicrophoneInput::onAudioInputStateChanged(QAudio::State state)
+{
+ switch (state) {
+ case QAudio::StoppedState:
+ if (mAudioInput->error() != QAudio::NoError) {
+ emit error(getErrorString(mAudioInput->error()));
+ }
+ break;
+
+ // handle other states?
+
+ default:
+ break;
+ }
+}
+
+/**
+ * @brief Calculate the current audio level of an array of samples and return it
+ * @param data An array of audio samples
+ * @return A value between 0 and 255
+ *
+ * Audio level is calculated as the RMS (root mean square) of the samples
+ * in the supplied array.
+ */
+quint8 UBMicrophoneInput::audioLevel(const QByteArray &data)
+{
+ int bytesPerSample = mAudioFormat.bytesPerFrame() / mAudioFormat.channelCount();
+
+ const char * ptr = data.constData();
+ double sum = 0;
+ int n_samples = data.size() / bytesPerSample;
+
+ for (int i(0); i < (data.size() - bytesPerSample); i += bytesPerSample) {
+ sum += pow(sampleRelativeLevel(ptr + i), 2);
+ }
+
+ double rms = sqrt(sum/n_samples);
+
+ // The vu meter looks a bit better when the RMS isn't displayed linearly, as perceived sound
+ // level increases logarithmically. So here RMS is substituted by rms^(1/e)
+ rms = pow(rms, 1./exp(1));
+
+ return UINT8_MAX * rms;
+}
+
+/**
+ * @brief Calculate one sample's level relative to its maximum value
+ * @param sample One sample, in the format specified by mAudioFormat
+ * @return A double between 0 and 1.0, where 1.0 is the maximum value the sample can take,
+ * or -1 if the value couldn't be calculated.
+ */
+double UBMicrophoneInput::sampleRelativeLevel(const char* sample)
+{
+ QAudioFormat::SampleType type = mAudioFormat.sampleType();
+ int sampleSize = mAudioFormat.sampleSize();
+
+ if (sampleSize == 16 && type == QAudioFormat::SignedInt)
+ return double(*reinterpret_cast(sample))/INT16_MAX;
+
+ if (sampleSize == 8 && type == QAudioFormat::SignedInt)
+ return double(*reinterpret_cast(sample))/INT8_MAX;
+
+ if (sampleSize == 16 && type == QAudioFormat::UnSignedInt)
+ return double(*reinterpret_cast(sample))/UINT16_MAX;
+
+ if (sampleSize == 8 && type == QAudioFormat::UnSignedInt)
+ return double(*reinterpret_cast(sample))/UINT8_MAX;
+
+ if (type == QAudioFormat::Float)
+ return (*reinterpret_cast(sample) + 1.0)/2.;
+
+ return -1;
+}
+
+/**
+ * @brief Return a meaningful error string based on QAudio error codes
+ */
+QString UBMicrophoneInput::getErrorString(QAudio::Error errorCode)
+{
+ switch (errorCode) {
+ case QAudio::NoError :
+ return "";
+
+ case QAudio::OpenError :
+ return "Couldn't open the audio device";
+
+ case QAudio::IOError :
+ return "Error reading from audio device";
+
+ case QAudio::UnderrunError :
+ return "Underrun error";
+
+ case QAudio::FatalError :
+ return "Fatal error; audio device unusable";
+
+ }
+ return "";
+}
diff --git a/src/podcast/ffmpeg/UBMicrophoneInput.h b/src/podcast/ffmpeg/UBMicrophoneInput.h
new file mode 100644
index 00000000..b82f5e70
--- /dev/null
+++ b/src/podcast/ffmpeg/UBMicrophoneInput.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015-2016 Département de l'Instruction Publique (DIP-SEM)
+ *
+ * This file is part of OpenBoard.
+ *
+ * OpenBoard 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, version 3 of the License,
+ * with a specific linking exception for the OpenSSL project's
+ * "OpenSSL" library (or with modified versions of it that use the
+ * same license as the "OpenSSL" library).
+ *
+ * OpenBoard 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenBoard. If not, see .
+ */
+
+#ifndef UBMICROPHONEINPUT_H
+#define UBMICROPHONEINPUT_H
+
+#include
+#include
+
+/**
+ * @brief The UBMicrophoneInput class captures uncompressed sound from a microphone.
+ *
+ * Audio samples can be read by connecting to the dataAvailable signal.
+ */
+class UBMicrophoneInput : public QObject
+{
+ Q_OBJECT
+
+public:
+ UBMicrophoneInput();
+ virtual ~UBMicrophoneInput();
+
+ bool init();
+ void start();
+ void stop();
+
+ static QStringList availableDevicesNames();
+ void setInputDevice(QString name = "");
+
+ int channelCount();
+ int sampleRate();
+ int sampleSize();
+ int sampleFormat();
+ QString codec();
+
+signals:
+ /// Send the new audio level, between 0 and 255
+ void audioLevelChanged(quint8 level);
+
+ /// Emitted when new audio data is available
+ void dataAvailable(QByteArray data);
+
+ void error(QString message);
+
+private slots:
+ void onAudioInputStateChanged(QAudio::State state);
+ void onDataReady();
+
+private:
+ double sampleRelativeLevel(const char* sample);
+ quint8 audioLevel(const QByteArray& data);
+ QString getErrorString(QAudio::Error errorCode);
+
+ QAudioInput* mAudioInput;
+ QIODevice * mIODevice;
+ QAudioDeviceInfo mAudioDeviceInfo;
+ QAudioFormat mAudioFormat;
+
+ qint64 mSeekPos;
+ quint8 mLastAudioLevel;
+};
+
+#endif // UBMICROPHONEINPUT_H
diff --git a/src/podcast/podcast.pri b/src/podcast/podcast.pri
index af441814..5b10ee9b 100644
--- a/src/podcast/podcast.pri
+++ b/src/podcast/podcast.pri
@@ -3,13 +3,15 @@ HEADERS += src/podcast/UBPodcastController.h \
src/podcast/UBAbstractVideoEncoder.h \
src/podcast/UBPodcastRecordingPalette.h \
src/podcast/youtube/UBYouTubePublisher.h \
- src/podcast/intranet/UBIntranetPodcastPublisher.h
+ src/podcast/intranet/UBIntranetPodcastPublisher.h \
+ $$PWD/ffmpeg/UBMicrophoneInput.h
SOURCES += src/podcast/UBPodcastController.cpp \
src/podcast/UBAbstractVideoEncoder.cpp \
src/podcast/UBPodcastRecordingPalette.cpp \
src/podcast/youtube/UBYouTubePublisher.cpp \
- src/podcast/intranet/UBIntranetPodcastPublisher.cpp
+ src/podcast/intranet/UBIntranetPodcastPublisher.cpp \
+ $$PWD/ffmpeg/UBMicrophoneInput.cpp
win32 {
@@ -33,3 +35,28 @@ macx {
OBJECTIVE_SOURCES += src/podcast/quicktime/UBQuickTimeFile.mm
}
+
+linux-g++* {
+ HEADERS += src/podcast/ffmpeg/UBFFmpegVideoEncoder.h
+
+ SOURCES += src/podcast/ffmpeg/UBFFmpegVideoEncoder.cpp
+
+
+ FFMPEG = /opt/ffmpeg
+
+ INCLUDEPATH += $${FFMPEG}/include
+ DEPENDPATH += /usr/lib/x86_64-linux-gnu
+
+ LIBS += -L $${FFMPEG}/lib -lavformat \
+ -L $${FFMPEG}/lib -lavcodec \
+ -L $${FFMPEG}/lib -lswscale \
+ -L $${FFMPEG}/lib -lavutil \
+ -lva-x11 \
+ -lva \
+ -lxcb-shm \
+ -lxcb-xfixes \
+ -lxcb-render -lxcb-shape -lxcb -lX11 -lasound -lSDL -lx264 -lpthread -lvpx -lvorbisenc -lvorbis -ltheoraenc -ltheoradec -logg -lopus -lmp3lame -lfreetype -lfdk-aac -lass -llzma -lbz2 -lz -ldl -lswresample -lswscale -lavutil -lm
+
+
+ QMAKE_CXXFLAGS += -std=c++11 # move this to OpenBoard.pro when we can use C++11 on all platforms
+}