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 +}