From b6089f3b915fea64897d2683e8f30f83ac3b9695 Mon Sep 17 00:00:00 2001 From: csoler Date: Sun, 13 Jul 2014 13:57:25 +0000 Subject: [PATCH] Started implementation of Video Chat (not working yet!). - GUI part is done - implemented a very basic JPEG codec - added echo frame in configuration panel - created a video capture object that uses OpenCV (should be cross systems) Remains to do: - serialise and send frames through p3VoRS - use a serious codec (e.g. Theora+x264) - add icons to reflect camera state (failure/working/sending/...) - compilation on windows git-svn-id: http://svn.code.sf.net/p/retroshare/code/trunk@7449 b45a01b8-16f6-495d-af2f-9b41ad6348cc --- plugins/VOIP/VOIP.pro | 11 +- plugins/VOIP/VOIPPlugin.cpp | 7 +- plugins/VOIP/gui/AudioChatWidgetHolder.cpp | 238 ---------------- plugins/VOIP/gui/AudioChatWidgetHolder.h | 41 --- plugins/VOIP/gui/AudioInputConfig.cpp | 56 ++-- plugins/VOIP/gui/AudioInputConfig.h | 8 +- plugins/VOIP/gui/AudioInputConfig.ui | 263 ++++++++++-------- plugins/VOIP/gui/PluginGUIHandler.cpp | 4 +- plugins/VOIP/gui/QVideoDevice.cpp | 102 +++++++ plugins/VOIP/gui/QVideoDevice.h | 52 ++++ plugins/VOIP/gui/VOIPChatWidgetHolder.cpp | 307 +++++++++++++++++++++ plugins/VOIP/gui/VOIPChatWidgetHolder.h | 62 +++++ plugins/VOIP/gui/VOIP_images.qrc | 3 + plugins/VOIP/gui/VideoProcessor.cpp | 60 ++++ plugins/VOIP/gui/VideoProcessor.h | 79 ++++++ plugins/VOIP/gui/images/camera-off.png | Bin 0 -> 3476 bytes plugins/VOIP/gui/images/camera-on.png | Bin 0 -> 4092 bytes plugins/VOIP/gui/images/video-icon-big.png | Bin 0 -> 45675 bytes 18 files changed, 873 insertions(+), 420 deletions(-) delete mode 100644 plugins/VOIP/gui/AudioChatWidgetHolder.cpp delete mode 100644 plugins/VOIP/gui/AudioChatWidgetHolder.h create mode 100644 plugins/VOIP/gui/QVideoDevice.cpp create mode 100644 plugins/VOIP/gui/QVideoDevice.h create mode 100644 plugins/VOIP/gui/VOIPChatWidgetHolder.cpp create mode 100644 plugins/VOIP/gui/VOIPChatWidgetHolder.h create mode 100644 plugins/VOIP/gui/VideoProcessor.cpp create mode 100644 plugins/VOIP/gui/VideoProcessor.h create mode 100644 plugins/VOIP/gui/images/camera-off.png create mode 100644 plugins/VOIP/gui/images/camera-on.png create mode 100644 plugins/VOIP/gui/images/video-icon-big.png diff --git a/plugins/VOIP/VOIP.pro b/plugins/VOIP/VOIP.pro index 728f42f68..24b7bb015 100644 --- a/plugins/VOIP/VOIP.pro +++ b/plugins/VOIP/VOIP.pro @@ -14,6 +14,7 @@ CONFIG += qt uic qrc resources MOBILITY = multimedia INCLUDEPATH += ../../retroshare-gui/src/temp/ui ../../libretroshare/src +INCLUDEPATH += /usr/include/opencv #################################### Windows ##################################### @@ -32,9 +33,11 @@ SOURCES = services/p3vors.cc \ gui/SpeexProcessor.cpp \ gui/audiodevicehelper.cpp \ gui/VoipStatistics.cpp \ - gui/AudioChatWidgetHolder.cpp \ + gui/VOIPChatWidgetHolder.cpp \ gui/PluginGUIHandler.cpp \ gui/PluginNotifier.cpp \ + gui/VideoProcessor.cpp \ + gui/QVideoDevice.cpp \ VOIPPlugin.cpp HEADERS = services/p3vors.h \ @@ -45,9 +48,11 @@ HEADERS = services/p3vors.h \ gui/SpeexProcessor.h \ gui/audiodevicehelper.h \ gui/VoipStatistics.h \ - gui/AudioChatWidgetHolder.h \ + gui/VOIPChatWidgetHolder.h \ gui/PluginGUIHandler.h \ gui/PluginNotifier.h \ + gui/VideoProcessor.h \ + gui/QVideoDevice.h \ interface/rsvoip.h \ VOIPPlugin.h @@ -81,4 +86,4 @@ TRANSLATIONS += \ lang/VOIP_tr.ts \ lang/VOIP_zh_CN.ts -LIBS += -lspeex -lspeexdsp +LIBS += -lspeex -lspeexdsp -lopencv_core -lopencv_highgui diff --git a/plugins/VOIP/VOIPPlugin.cpp b/plugins/VOIP/VOIPPlugin.cpp index aab4d713a..e3843a53b 100644 --- a/plugins/VOIP/VOIPPlugin.cpp +++ b/plugins/VOIP/VOIPPlugin.cpp @@ -12,7 +12,7 @@ #include "gui/VoipStatistics.h" #include "gui/AudioInputConfig.h" -#include "gui/AudioChatWidgetHolder.h" +#include "gui/VOIPChatWidgetHolder.h" #include "gui/PluginGUIHandler.h" #include "gui/PluginNotifier.h" #include "gui/SoundManager.h" @@ -80,6 +80,9 @@ void VOIPPlugin::setInterfaces(RsPlugInInterfaces &interfaces) ConfigPage *VOIPPlugin::qt_config_page() const { + // The config pages are deleted when config is closed, so it's important not to static the + // created object. + // return new AudioInputConfig() ; } @@ -111,7 +114,7 @@ ChatWidgetHolder *VOIPPlugin::qt_get_chat_widget_holder(ChatWidget *chatWidget) { switch (chatWidget->chatType()) { case ChatWidget::CHATTYPE_PRIVATE: - return new AudioChatWidgetHolder(chatWidget); + return new VOIPChatWidgetHolder(chatWidget); case ChatWidget::CHATTYPE_UNKNOWN: case ChatWidget::CHATTYPE_LOBBY: case ChatWidget::CHATTYPE_DISTANT: diff --git a/plugins/VOIP/gui/AudioChatWidgetHolder.cpp b/plugins/VOIP/gui/AudioChatWidgetHolder.cpp deleted file mode 100644 index 26e8bbbec..000000000 --- a/plugins/VOIP/gui/AudioChatWidgetHolder.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include -#include -#include - -#include "AudioChatWidgetHolder.h" -#include -#include "interface/rsvoip.h" -#include "gui/SoundManager.h" -#include "util/HandleRichText.h" -#include "gui/common/StatusDefs.h" -#include "gui/chat/ChatWidget.h" - -#include - -#define CALL_START ":/images/call-start-22.png" -#define CALL_STOP ":/images/call-stop-22.png" -#define CALL_HOLD ":/images/call-hold-22.png" - - -AudioChatWidgetHolder::AudioChatWidgetHolder(ChatWidget *chatWidget) - : QObject(), ChatWidgetHolder(chatWidget) -{ - audioListenToggleButton = new QToolButton ; - audioListenToggleButton->setMinimumSize(QSize(28,28)) ; - audioListenToggleButton->setMaximumSize(QSize(28,28)) ; - audioListenToggleButton->setText(QString()) ; - audioListenToggleButton->setToolTip(tr("Mute yourself")); - - std::cerr << "****** VOIPLugin: Creating new AudioChatWidgetHolder !!" << std::endl; - - QIcon icon ; - icon.addPixmap(QPixmap(":/images/audio-volume-muted-22.png")) ; - icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Normal,QIcon::On) ; - icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Disabled,QIcon::On) ; - icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Active,QIcon::On) ; - icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Selected,QIcon::On) ; - - audioListenToggleButton->setIcon(icon) ; - audioListenToggleButton->setIconSize(QSize(22,22)) ; - audioListenToggleButton->setAutoRaise(true) ; - audioListenToggleButton->setCheckable(true); - - audioMuteCaptureToggleButton = new QToolButton ; - audioMuteCaptureToggleButton->setMinimumSize(QSize(28,28)) ; - audioMuteCaptureToggleButton->setMaximumSize(QSize(28,28)) ; - audioMuteCaptureToggleButton->setText(QString()) ; - audioMuteCaptureToggleButton->setToolTip(tr("Start Call")); - - QIcon icon2 ; - icon2.addPixmap(QPixmap(":/images/call-start-22.png")) ; - icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Normal,QIcon::On) ; - icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Disabled,QIcon::On) ; - icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Active,QIcon::On) ; - icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Selected,QIcon::On) ; - - audioMuteCaptureToggleButton->setIcon(icon2) ; - audioMuteCaptureToggleButton->setIconSize(QSize(22,22)) ; - audioMuteCaptureToggleButton->setAutoRaise(true) ; - audioMuteCaptureToggleButton->setCheckable(true) ; - - hangupButton = new QToolButton ; - hangupButton->setIcon(QIcon(":/images/call-stop-22.png")) ; - hangupButton->setIconSize(QSize(22,22)) ; - hangupButton->setMinimumSize(QSize(28,28)) ; - hangupButton->setMaximumSize(QSize(28,28)) ; - hangupButton->setCheckable(false) ; - hangupButton->setAutoRaise(true) ; - hangupButton->setText(QString()) ; - hangupButton->setToolTip(tr("Hangup Call")); - - connect(audioListenToggleButton, SIGNAL(clicked()), this , SLOT(toggleAudioListen())); - connect(audioMuteCaptureToggleButton, SIGNAL(clicked()), this , SLOT(toggleAudioMuteCapture())); - connect(hangupButton, SIGNAL(clicked()), this , SLOT(hangupCall())); - - mChatWidget->addChatBarWidget(audioListenToggleButton) ; - mChatWidget->addChatBarWidget(audioMuteCaptureToggleButton) ; - mChatWidget->addChatBarWidget(hangupButton) ; - - outputProcessor = NULL ; - outputDevice = NULL ; - inputProcessor = NULL ; - inputDevice = NULL ; -} - -AudioChatWidgetHolder::~AudioChatWidgetHolder() -{ - if(inputDevice != NULL) - inputDevice->stop() ; -} - -void AudioChatWidgetHolder::toggleAudioListen() -{ - std::cerr << "******** VOIPLugin: Toggling audio listen!" << std::endl; - if (audioListenToggleButton->isChecked()) { - audioListenToggleButton->setToolTip(tr("Mute yourself")); - } else { - audioListenToggleButton->setToolTip(tr("Unmute yourself")); - //audioListenToggleButton->setChecked(false); - /*if (outputDevice) { - outputDevice->stop(); - }*/ - } -} - -void AudioChatWidgetHolder::hangupCall() -{ - std::cerr << "******** VOIPLugin: Hangup call!" << std::endl; - - disconnect(inputProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); - if (inputDevice) { - inputDevice->stop(); - } - if (outputDevice) { - outputDevice->stop(); - } - audioListenToggleButton->setChecked(false); - audioMuteCaptureToggleButton->setChecked(false); -} - -void AudioChatWidgetHolder::toggleAudioMuteCapture() -{ - std::cerr << "******** VOIPLugin: Toggling audio mute capture!" << std::endl; - if (audioMuteCaptureToggleButton->isChecked()) { - //activate audio output - audioListenToggleButton->setChecked(true); - audioMuteCaptureToggleButton->setToolTip(tr("Hold Call")); - - //activate audio input - if (!inputProcessor) { - inputProcessor = new QtSpeex::SpeexInputProcessor(); - if (outputProcessor) { - connect(outputProcessor, SIGNAL(playingFrame(QByteArray*)), inputProcessor, SLOT(addEchoFrame(QByteArray*))); - } - inputProcessor->open(QIODevice::WriteOnly | QIODevice::Unbuffered); - } - if (!inputDevice) { - inputDevice = AudioDeviceHelper::getPreferedInputDevice(); - } - connect(inputProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); - inputDevice->start(inputProcessor); - - if (mChatWidget) { - mChatWidget->addChatMsg(true, tr("VoIP Status"), QDateTime::currentDateTime(), QDateTime::currentDateTime(), tr("Outgoing Call is started..."), ChatWidget::MSGTYPE_SYSTEM); - } - - } else { - disconnect(inputProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); - if (inputDevice) { - inputDevice->stop(); - } - audioMuteCaptureToggleButton->setToolTip(tr("Resume Call")); - } -} - -void AudioChatWidgetHolder::addAudioData(const QString name, QByteArray* array) -{ - if (!audioMuteCaptureToggleButton->isChecked()) { - //launch an animation. Don't launch it if already animating - if (!audioMuteCaptureToggleButton->graphicsEffect() || - (audioMuteCaptureToggleButton->graphicsEffect()->inherits("QGraphicsOpacityEffect") && - ((QGraphicsOpacityEffect*)audioMuteCaptureToggleButton->graphicsEffect())->opacity() == 1) - ) { - QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(audioListenToggleButton); - audioMuteCaptureToggleButton->setGraphicsEffect(effect); - QPropertyAnimation *anim = new QPropertyAnimation(effect, "opacity"); - anim->setStartValue(1); - anim->setKeyValueAt(0.5,0); - anim->setEndValue(1); - anim->setDuration(400); - anim->start(); - } - -// soundManager->play(VOIP_SOUND_INCOMING_CALL); - - audioMuteCaptureToggleButton->setToolTip(tr("Answer")); - - //TODO make a toaster and a sound for the incoming call - return; - } - - if (!outputDevice) { - outputDevice = AudioDeviceHelper::getDefaultOutputDevice(); - } - - if (!outputProcessor) { - //start output audio device - outputProcessor = new QtSpeex::SpeexOutputProcessor(); - if (inputProcessor) { - connect(outputProcessor, SIGNAL(playingFrame(QByteArray*)), inputProcessor, SLOT(addEchoFrame(QByteArray*))); - } - outputProcessor->open(QIODevice::ReadOnly | QIODevice::Unbuffered); - outputDevice->start(outputProcessor); - } - - if (outputDevice && outputDevice->error() != QAudio::NoError) { - std::cerr << "Restarting output device. Error before reset " << outputDevice->error() << " buffer size : " << outputDevice->bufferSize() << std::endl; - outputDevice->stop(); - outputDevice->reset(); - if (outputDevice->error() == QAudio::UnderrunError) - outputDevice->setBufferSize(20); - outputDevice->start(outputProcessor); - } - outputProcessor->putNetworkPacket(name, *array); - - //check the input device for errors - if (inputDevice && inputDevice->error() != QAudio::NoError) { - std::cerr << "Restarting input device. Error before reset " << inputDevice->error() << std::endl; - inputDevice->stop(); - inputDevice->reset(); - inputDevice->start(inputProcessor); - } -} - -void AudioChatWidgetHolder::sendAudioData() -{ - while(inputProcessor && inputProcessor->hasPendingPackets()) { - QByteArray qbarray = inputProcessor->getNetworkPacket(); - RsVoipDataChunk chunk; - chunk.size = qbarray.size(); - chunk.data = (void*)qbarray.constData(); - rsVoip->sendVoipData(mChatWidget->getPeerId(),chunk); - } -} - -void AudioChatWidgetHolder::updateStatus(int status) -{ - audioListenToggleButton->setEnabled(true); - audioMuteCaptureToggleButton->setEnabled(true); - hangupButton->setEnabled(true); - - switch (status) { - case RS_STATUS_OFFLINE: - audioListenToggleButton->setEnabled(false); - audioMuteCaptureToggleButton->setEnabled(false); - hangupButton->setEnabled(false); - break; - } -} diff --git a/plugins/VOIP/gui/AudioChatWidgetHolder.h b/plugins/VOIP/gui/AudioChatWidgetHolder.h deleted file mode 100644 index 63c1e08af..000000000 --- a/plugins/VOIP/gui/AudioChatWidgetHolder.h +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include -#include -#include - -class QToolButton; -class QAudioInput; -class QAudioOutput; - -#define VOIP_SOUND_INCOMING_CALL "VOIP_incoming_call" - -class AudioChatWidgetHolder : public QObject, public ChatWidgetHolder -{ - Q_OBJECT - -public: - AudioChatWidgetHolder(ChatWidget *chatWidget); - virtual ~AudioChatWidgetHolder(); - - virtual void updateStatus(int status); - - void addAudioData(const QString name, QByteArray* array) ; - -private slots: - void toggleAudioListen(); - void toggleAudioMuteCapture(); - void hangupCall() ; - -public slots: - void sendAudioData(); - -protected: - QAudioInput* inputDevice; - QAudioOutput* outputDevice; - QtSpeex::SpeexInputProcessor* inputProcessor; - QtSpeex::SpeexOutputProcessor* outputProcessor; - - QToolButton *audioListenToggleButton ; - QToolButton *audioMuteCaptureToggleButton ; - QToolButton *hangupButton ; -}; diff --git a/plugins/VOIP/gui/AudioInputConfig.cpp b/plugins/VOIP/gui/AudioInputConfig.cpp index 564c305c9..73f6d0ae2 100644 --- a/plugins/VOIP/gui/AudioInputConfig.cpp +++ b/plugins/VOIP/gui/AudioInputConfig.cpp @@ -55,28 +55,43 @@ void AudioInputDialog::showEvent(QShowEvent *) { AudioInputConfig::AudioInputConfig(QWidget * parent, Qt::WindowFlags flags) : ConfigPage(parent, flags) { + std::cerr << "Creating audioInputConfig object" << std::endl; + /* Invoke the Qt Designer generated object setup routine */ ui.setupUi(this); loaded = false; - inputProcessor = NULL; - inputDevice = NULL; + inputAudioProcessor = NULL; + inputAudioDevice = NULL; abSpeech = NULL; + + // Create the video pipeline. + // + videoInput = new QVideoInputDevice(this) ; + videoInput->setEchoVideoTarget(ui.videoDisplay) ; + videoInput->setVideoEncoder(NULL) ; } AudioInputConfig::~AudioInputConfig() { - if (inputDevice) { - inputDevice->stop(); - delete inputDevice ; - inputDevice = NULL ; + std::cerr << "Deleting audioInputConfig object" << std::endl; + if(videoInput != NULL) + { + videoInput->stop() ; + delete videoInput ; + } + + if (inputAudioDevice) { + inputAudioDevice->stop(); + delete inputAudioDevice ; + inputAudioDevice = NULL ; } - if(inputProcessor) + if(inputAudioProcessor) { - delete inputProcessor ; - inputProcessor = NULL ; + delete inputAudioProcessor ; + inputAudioProcessor = NULL ; } } @@ -168,6 +183,9 @@ void AudioInputConfig::loadSettings() { connect( ui.qsAmp, SIGNAL( valueChanged ( int ) ), this, SLOT( on_qsAmp_valueChanged(int) ) ); connect( ui.qcbTransmit, SIGNAL( currentIndexChanged ( int ) ), this, SLOT( on_qcbTransmit_currentIndexChanged(int) ) ); loaded = true; + + std::cerr << "AudioInputConfig:: starting video." << std::endl; + videoInput->start() ; } bool AudioInputConfig::save(QString &/*errmsg*/) {//mainly useless beacause saving occurs in realtime @@ -248,15 +266,15 @@ void AudioInputConfig::on_qcbTransmit_currentIndexChanged(int v) { void AudioInputConfig::on_Tick_timeout() { - if (!inputProcessor) { - inputProcessor = new QtSpeex::SpeexInputProcessor(); - inputProcessor->open(QIODevice::WriteOnly | QIODevice::Unbuffered); + if (!inputAudioProcessor) { + inputAudioProcessor = new QtSpeex::SpeexInputProcessor(); + inputAudioProcessor->open(QIODevice::WriteOnly | QIODevice::Unbuffered); - if (!inputDevice) { - inputDevice = AudioDeviceHelper::getPreferedInputDevice(); + if (!inputAudioDevice) { + inputAudioDevice = AudioDeviceHelper::getPreferedInputDevice(); } - inputDevice->start(inputProcessor); - connect(inputProcessor, SIGNAL(networkPacketReady()), this, SLOT(emptyBuffer())); + inputAudioDevice->start(inputAudioProcessor); + connect(inputAudioProcessor, SIGNAL(networkPacketReady()), this, SLOT(emptyBuffer())); } abSpeech->iBelow = ui.qsTransmitMin->value(); @@ -266,14 +284,14 @@ void AudioInputConfig::on_Tick_timeout() { rsVoip->setVoipfVADmax(ui.qsTransmitMax->value()); } - abSpeech->iValue = iroundf(inputProcessor->dVoiceAcivityLevel * 32767.0f + 0.5f); + abSpeech->iValue = iroundf(inputAudioProcessor->dVoiceAcivityLevel * 32767.0f + 0.5f); abSpeech->update(); } void AudioInputConfig::emptyBuffer() { - while(inputProcessor->hasPendingPackets()) { - inputProcessor->getNetworkPacket(); //that will purge the buffer + while(inputAudioProcessor->hasPendingPackets()) { + inputAudioProcessor->getNetworkPacket(); //that will purge the buffer } } diff --git a/plugins/VOIP/gui/AudioInputConfig.h b/plugins/VOIP/gui/AudioInputConfig.h index 6acbf1a1d..2d605c42a 100644 --- a/plugins/VOIP/gui/AudioInputConfig.h +++ b/plugins/VOIP/gui/AudioInputConfig.h @@ -38,6 +38,7 @@ #include "ui_AudioInputConfig.h" #include "SpeexProcessor.h" +#include "VideoProcessor.h" #include "AudioStats.h" class AudioInputConfig : public ConfigPage @@ -46,9 +47,12 @@ class AudioInputConfig : public ConfigPage private: Ui::AudioInput ui; - QAudioInput* inputDevice; - QtSpeex::SpeexInputProcessor* inputProcessor; + QAudioInput* inputAudioDevice; + QtSpeex::SpeexInputProcessor* inputAudioProcessor; AudioBar* abSpeech; + //VideoDecoder *videoDecoder ; + //VideoEncoder *videoEncoder ; + QVideoInputDevice *videoInput ; bool loaded; diff --git a/plugins/VOIP/gui/AudioInputConfig.ui b/plugins/VOIP/gui/AudioInputConfig.ui index 68e557b5c..6aa071b4a 100644 --- a/plugins/VOIP/gui/AudioInputConfig.ui +++ b/plugins/VOIP/gui/AudioInputConfig.ui @@ -6,11 +6,11 @@ 0 0 - 508 - 378 + 501 + 406 - + @@ -235,116 +235,145 @@ - - - Audio Processing - - - - - - Noise Suppression - - - qsNoise - - - - - - - true - - - Noise suppression - - - <b>This sets the amount of noise suppression to apply.</b><br />The higher this value, the more aggressively stationary noise will be suppressed. - - - 14 - - - 60 - - - 5 - - - Qt::Horizontal - - - - - - - - 30 - 0 - - - - - - - - - - - Amplification - - - qsAmp - - - - - - - Maximum amplification of input sound - - - <b>Maximum amplification of input.</b><br />RetroShare normalizes the input volume before compressing, and this sets how much it's allowed to amplify.<br />The actual level is continually updated based on your current speech pattern, but it will never go above the level specified here.<br />If the <i>Microphone loudness</i> level of the audio statistics hover around 100%, you probably want to set this to 2.0 or so, but if, like most people, you are unable to reach 100%, set this to something much higher.<br />Ideally, set it so <i>Microphone Loudness * Amplification Factor >= 100</i>, even when you're speaking really soft.<br /><br />Note that there is no harm in setting this to maximum, but RetroShare will start picking up other conversations if you leave it to auto-tune to that level. - - - 19500 - - - 500 - - - 2000 - - - Qt::Horizontal - - - - - - - - 30 - 0 - - - - - - - - - - - Echo Cancellation Processing - - - false - - - - - + + + + + Audio Processing + + + + + + Noise Suppression + + + qsNoise + + + + + + + true + + + Noise suppression + + + <b>This sets the amount of noise suppression to apply.</b><br />The higher this value, the more aggressively stationary noise will be suppressed. + + + 14 + + + 60 + + + 5 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + + + + + + + + Amplification + + + qsAmp + + + + + + + Maximum amplification of input sound + + + <b>Maximum amplification of input.</b><br />RetroShare normalizes the input volume before compressing, and this sets how much it's allowed to amplify.<br />The actual level is continually updated based on your current speech pattern, but it will never go above the level specified here.<br />If the <i>Microphone loudness</i> level of the audio statistics hover around 100%, you probably want to set this to 2.0 or so, but if, like most people, you are unable to reach 100%, set this to something much higher.<br />Ideally, set it so <i>Microphone Loudness * Amplification Factor >= 100</i>, even when you're speaking really soft.<br /><br />Note that there is no harm in setting this to maximum, but RetroShare will start picking up other conversations if you leave it to auto-tune to that level. + + + 19500 + + + 500 + + + 2000 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + + + + + + + + Echo Cancellation Processing + + + false + + + + + + + + + + Video Processing + + + + + + + 170 + 128 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + @@ -361,6 +390,14 @@ + + + QVideoOutputDevice + QFrame +
gui/QVideoDevice.h
+ 1 +
+
qcbTransmit qsDoublePush diff --git a/plugins/VOIP/gui/PluginGUIHandler.cpp b/plugins/VOIP/gui/PluginGUIHandler.cpp index 17b0f8d71..075b0ea24 100644 --- a/plugins/VOIP/gui/PluginGUIHandler.cpp +++ b/plugins/VOIP/gui/PluginGUIHandler.cpp @@ -4,7 +4,7 @@ #include #include "PluginGUIHandler.h" #include -#include +#include #include "gui/chat/ChatWidget.h" void PluginGUIHandler::ReceivedInvitation(const QString& /*peer_id*/) @@ -42,7 +42,7 @@ void PluginGUIHandler::ReceivedVoipData(const QString& qpeer_id) const QList &chatWidgetHolderList = cw->chatWidgetHolderList(); foreach (ChatWidgetHolder *chatWidgetHolder, chatWidgetHolderList) { - AudioChatWidgetHolder *acwh = dynamic_cast(chatWidgetHolder) ; + VOIPChatWidgetHolder *acwh = dynamic_cast(chatWidgetHolder) ; if (acwh) { for (unsigned int i = 0; i < chunks.size(); ++i) { diff --git a/plugins/VOIP/gui/QVideoDevice.cpp b/plugins/VOIP/gui/QVideoDevice.cpp new file mode 100644 index 000000000..386415020 --- /dev/null +++ b/plugins/VOIP/gui/QVideoDevice.cpp @@ -0,0 +1,102 @@ +#include +#include + +#include +#include +#include "QVideoDevice.h" +#include "VideoProcessor.h" + +QVideoInputDevice::QVideoInputDevice(QWidget *parent) +{ + _timer = NULL ; + _capture_device = NULL ; + _video_encoder = NULL ; + _echo_output_device = NULL ; +} + +void QVideoInputDevice::stop() +{ + if(_timer != NULL) + { + QObject::disconnect(_timer,SIGNAL(timeout()),this,SLOT(grabFrame())) ; + _timer->stop() ; + delete _timer ; + _timer = NULL ; + } + if(_capture_device != NULL) + { + cvReleaseCapture(&_capture_device) ; + _capture_device = NULL ; + } +} +void QVideoInputDevice::start() +{ + // make sure everything is re-initialised + // + stop() ; + + // Initialise la capture + static const int cam_id = 0 ; + _capture_device = cvCaptureFromCAM(cam_id); + + if(_capture_device == NULL) + { + std::cerr << "Cannot initialise camera. Something's wrong." << std::endl; + return ; + } + + _timer = new QTimer ; + QObject::connect(_timer,SIGNAL(timeout()),this,SLOT(grabFrame())) ; + + _timer->start(50) ; // 10 images per second. +} + +void QVideoInputDevice::grabFrame() +{ + IplImage *img=cvQueryFrame(_capture_device); + + if(img == NULL) + { + std::cerr << "(EE) Cannot capture image from camera. Something's wrong." << std::endl; + return ; + } + // get the image data + + if(img->nChannels != 3) + { + std::cerr << "(EE) expected 3 channels. Got " << img->nChannels << std::endl; + cvReleaseImage(&img) ; + return ; + } + + static const int _encoded_width = 128 ; + static const int _encoded_height = 128 ; + + QImage image = QImage((uchar*)img->imageData,img->width,img->height,QImage::Format_RGB888).scaled(QSize(_encoded_width,_encoded_height),Qt::IgnoreAspectRatio,Qt::SmoothTransformation) ; + + if(_video_encoder != NULL) _video_encoder->addImage(image) ; + if(_echo_output_device != NULL) _echo_output_device->showFrame(image) ; +} + +QVideoInputDevice::~QVideoInputDevice() +{ + stop() ; +} + + +QVideoOutputDevice::QVideoOutputDevice(QWidget *parent) + : QLabel(parent) +{ + setPixmap(QPixmap(":/images/video-icon-big.png").scaled(170,128,Qt::KeepAspectRatio,Qt::SmoothTransformation)) ; +} + +void QVideoOutputDevice::showFrame(const QImage& img) +{ + //std::cerr << "Displaying frame!!" << std::endl; + + //QPainter painter(this) ; + //painter.drawImage(QPointF(0,0),img) ; + + setPixmap(QPixmap::fromImage(img).scaled(minimumSize(),Qt::IgnoreAspectRatio,Qt::SmoothTransformation)) ; +} + diff --git a/plugins/VOIP/gui/QVideoDevice.h b/plugins/VOIP/gui/QVideoDevice.h new file mode 100644 index 000000000..d454aa635 --- /dev/null +++ b/plugins/VOIP/gui/QVideoDevice.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +class VideoEncoder ; +class CvCapture ; + +// Responsible from displaying the video. The source of the video is +// a VideoDecoder object, which uses a codec. +// +class QVideoOutputDevice: public QLabel +{ + public: + QVideoOutputDevice(QWidget *parent) ; + + void showFrame(const QImage&) ; +}; + +// Responsible for grabbing the video from the webcam and sending it to the +// VideoEncoder object. +// +class QVideoInputDevice: public QObject +{ + Q_OBJECT + + public: + QVideoInputDevice(QWidget *parent) ; + ~QVideoInputDevice() ; + + // Captured images are sent to this encoder. Can be NULL. + // + void setVideoEncoder(VideoEncoder *venc) { _video_encoder = venc ; } + + // All images received will be echoed to this target. We could use signal/slots, but it's + // probably faster this way. Can be NULL. + // + void setEchoVideoTarget(QVideoOutputDevice *odev) { _echo_output_device = odev ; } + + void start() ; + void stop() ; + + protected slots: + void grabFrame() ; + + private: + VideoEncoder *_video_encoder ; + QTimer *_timer ; + CvCapture *_capture_device ; + + QVideoOutputDevice *_echo_output_device ; +}; + diff --git a/plugins/VOIP/gui/VOIPChatWidgetHolder.cpp b/plugins/VOIP/gui/VOIPChatWidgetHolder.cpp new file mode 100644 index 000000000..b914ef238 --- /dev/null +++ b/plugins/VOIP/gui/VOIPChatWidgetHolder.cpp @@ -0,0 +1,307 @@ +#include +#include +#include +#include + +#include +#include "interface/rsvoip.h" +#include "gui/SoundManager.h" +#include "util/HandleRichText.h" +#include "gui/common/StatusDefs.h" +#include "gui/chat/ChatWidget.h" + +#include "VOIPChatWidgetHolder.h" +#include "VideoProcessor.h" +#include "QVideoDevice.h" + +#include + +#define CALL_START ":/images/call-start-22.png" +#define CALL_STOP ":/images/call-stop-22.png" +#define CALL_HOLD ":/images/call-hold-22.png" + + +VOIPChatWidgetHolder::VOIPChatWidgetHolder(ChatWidget *chatWidget) + : QObject(), ChatWidgetHolder(chatWidget) +{ + std::cerr << "****** VOIPLugin: Creating new VOIPChatWidgetHolder !!" << std::endl; + + QIcon icon ; + icon.addPixmap(QPixmap(":/images/audio-volume-muted-22.png")) ; + icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Normal,QIcon::On) ; + icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Disabled,QIcon::On) ; + icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Active,QIcon::On) ; + icon.addPixmap(QPixmap(":/images/audio-volume-medium-22.png"),QIcon::Selected,QIcon::On) ; + + audioListenToggleButton = new QToolButton ; + audioListenToggleButton->setIcon(icon) ; + audioListenToggleButton->setIconSize(QSize(22,22)) ; + audioListenToggleButton->setAutoRaise(true) ; + audioListenToggleButton->setCheckable(true); + audioListenToggleButton->setMinimumSize(QSize(28,28)) ; + audioListenToggleButton->setMaximumSize(QSize(28,28)) ; + audioListenToggleButton->setText(QString()) ; + audioListenToggleButton->setToolTip(tr("Mute")); + + QIcon icon2 ; + icon2.addPixmap(QPixmap(":/images/call-start-22.png")) ; + icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Normal,QIcon::On) ; + icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Disabled,QIcon::On) ; + icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Active,QIcon::On) ; + icon2.addPixmap(QPixmap(":/images/call-hold-22.png"),QIcon::Selected,QIcon::On) ; + + audioCaptureToggleButton = new QToolButton ; + audioCaptureToggleButton->setMinimumSize(QSize(28,28)) ; + audioCaptureToggleButton->setMaximumSize(QSize(28,28)) ; + audioCaptureToggleButton->setText(QString()) ; + audioCaptureToggleButton->setToolTip(tr("Start Call")); + audioCaptureToggleButton->setIcon(icon2) ; + audioCaptureToggleButton->setIconSize(QSize(22,22)) ; + audioCaptureToggleButton->setAutoRaise(true) ; + audioCaptureToggleButton->setCheckable(true) ; + + QIcon icon3 ; + icon3.addPixmap(QPixmap(":/images/camera-on.png")) ; + icon3.addPixmap(QPixmap(":/images/camera-off.png"),QIcon::Normal,QIcon::On) ; + icon3.addPixmap(QPixmap(":/images/camera-off.png"),QIcon::Disabled,QIcon::On) ; + icon3.addPixmap(QPixmap(":/images/camera-off.png"),QIcon::Active,QIcon::On) ; + icon3.addPixmap(QPixmap(":/images/camera-off.png"),QIcon::Selected,QIcon::On) ; + + videoCaptureToggleButton = new QToolButton ; + videoCaptureToggleButton->setMinimumSize(QSize(28,28)) ; + videoCaptureToggleButton->setMaximumSize(QSize(28,28)) ; + videoCaptureToggleButton->setText(QString()) ; + videoCaptureToggleButton->setToolTip(tr("Start Call")); + videoCaptureToggleButton->setIcon(icon3) ; + videoCaptureToggleButton->setIconSize(QSize(22,22)) ; + videoCaptureToggleButton->setAutoRaise(true) ; + videoCaptureToggleButton->setCheckable(true) ; + + hangupButton = new QToolButton ; + hangupButton->setIcon(QIcon(":/images/call-stop-22.png")) ; + hangupButton->setIconSize(QSize(22,22)) ; + hangupButton->setMinimumSize(QSize(28,28)) ; + hangupButton->setMaximumSize(QSize(28,28)) ; + hangupButton->setCheckable(false) ; + hangupButton->setAutoRaise(true) ; + hangupButton->setText(QString()) ; + hangupButton->setToolTip(tr("Hangup Call")); + + connect(videoCaptureToggleButton, SIGNAL(clicked()), this , SLOT(toggleVideoCapture())); + connect(audioListenToggleButton, SIGNAL(clicked()), this , SLOT(toggleAudioListen())); + connect(audioCaptureToggleButton, SIGNAL(clicked()), this , SLOT(toggleAudioCapture())); + connect(hangupButton, SIGNAL(clicked()), this , SLOT(hangupCall())); + + mChatWidget->addChatBarWidget(audioListenToggleButton) ; + mChatWidget->addChatBarWidget(audioCaptureToggleButton) ; + mChatWidget->addChatBarWidget(videoCaptureToggleButton) ; + mChatWidget->addChatBarWidget(hangupButton) ; + + outputAudioProcessor = NULL ; + outputAudioDevice = NULL ; + inputAudioProcessor = NULL ; + inputAudioDevice = NULL ; + + inputVideoDevice = new QVideoInputDevice(mChatWidget) ; // not started yet ;-) + inputVideoProcessor = new JPEGVideoEncoder ; + outputVideoProcessor = new JPEGVideoDecoder ; + + // Make a widget with two video devices, one for echo, and one for the talking peer. + videoWidget = new QWidget(mChatWidget) ; + videoWidget->setLayout(new QHBoxLayout()) ; + videoWidget->layout()->addWidget(echoVideoDevice = new QVideoOutputDevice(videoWidget)) ; + videoWidget->layout()->addWidget(outputVideoDevice = new QVideoOutputDevice(videoWidget)) ; + + echoVideoDevice->setMinimumSize(128,95) ; + outputVideoDevice->setMinimumSize(128,95) ; + + mChatWidget->addChatHorizontalWidget(videoWidget) ; + + inputVideoDevice->setEchoVideoTarget(echoVideoDevice) ; + outputVideoProcessor->setDisplayTarget(outputVideoDevice) ; +} + +VOIPChatWidgetHolder::~VOIPChatWidgetHolder() +{ + if(inputAudioDevice != NULL) + inputAudioDevice->stop() ; + + delete inputVideoDevice ; + delete inputVideoProcessor ; + delete outputVideoProcessor ; +} + +void VOIPChatWidgetHolder::toggleAudioListen() +{ + std::cerr << "******** VOIPLugin: Toggling audio listen!" << std::endl; + if (audioListenToggleButton->isChecked()) { + audioListenToggleButton->setToolTip(tr("Mute yourself")); + } else { + audioListenToggleButton->setToolTip(tr("Unmute yourself")); + //audioListenToggleButton->setChecked(false); + /*if (outputAudioDevice) { + outputAudioDevice->stop(); + }*/ + } +} + +void VOIPChatWidgetHolder::hangupCall() +{ + std::cerr << "******** VOIPLugin: Hangup call!" << std::endl; + + disconnect(inputAudioProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); + if (inputAudioDevice) { + inputAudioDevice->stop(); + } + if (outputAudioDevice) { + outputAudioDevice->stop(); + } + audioListenToggleButton->setChecked(false); + audioCaptureToggleButton->setChecked(false); +} + +void VOIPChatWidgetHolder::toggleAudioCapture() +{ + std::cerr << "******** VOIPLugin: Toggling audio mute capture!" << std::endl; + if (audioCaptureToggleButton->isChecked()) { + //activate audio output + audioListenToggleButton->setChecked(true); + audioCaptureToggleButton->setToolTip(tr("Hold Call")); + + //activate audio input + if (!inputAudioProcessor) { + inputAudioProcessor = new QtSpeex::SpeexInputProcessor(); + if (outputAudioProcessor) { + connect(outputAudioProcessor, SIGNAL(playingFrame(QByteArray*)), inputAudioProcessor, SLOT(addEchoFrame(QByteArray*))); + } + inputAudioProcessor->open(QIODevice::WriteOnly | QIODevice::Unbuffered); + } + if (!inputAudioDevice) { + inputAudioDevice = AudioDeviceHelper::getPreferedInputDevice(); + } + connect(inputAudioProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); + inputAudioDevice->start(inputAudioProcessor); + + if (mChatWidget) { + mChatWidget->addChatMsg(true, tr("VoIP Status"), QDateTime::currentDateTime(), QDateTime::currentDateTime(), tr("Outgoing Call is started..."), ChatWidget::MSGTYPE_SYSTEM); + } + + } else { + disconnect(inputAudioProcessor, SIGNAL(networkPacketReady()), this, SLOT(sendAudioData())); + if (inputAudioDevice) { + inputAudioDevice->stop(); + } + audioCaptureToggleButton->setToolTip(tr("Resume Call")); + } +} +void VOIPChatWidgetHolder::toggleVideoCapture() +{ + std::cerr << "******** VOIPLugin: Toggling video capture!" << std::endl; + + if (videoCaptureToggleButton->isChecked()) + { + //activate video input + // + inputVideoDevice->start() ; + + videoCaptureToggleButton->setToolTip(tr("Shut camera off")); + + if (mChatWidget) + mChatWidget->addChatMsg(true, tr("VoIP Status"), QDateTime::currentDateTime(), QDateTime::currentDateTime(), tr("you're now sending video..."), ChatWidget::MSGTYPE_SYSTEM); + } + else + { + if(inputVideoDevice) + { + delete inputVideoDevice ; + inputVideoDevice = NULL ; + } + + videoCaptureToggleButton->setToolTip(tr("Activate camera")); + } +} + +void VOIPChatWidgetHolder::addAudioData(const QString name, QByteArray* array) +{ + if (!audioCaptureToggleButton->isChecked()) { + //launch an animation. Don't launch it if already animating + if (!audioCaptureToggleButton->graphicsEffect() || + (audioCaptureToggleButton->graphicsEffect()->inherits("QGraphicsOpacityEffect") && + ((QGraphicsOpacityEffect*)audioCaptureToggleButton->graphicsEffect())->opacity() == 1) + ) { + QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(audioListenToggleButton); + audioCaptureToggleButton->setGraphicsEffect(effect); + QPropertyAnimation *anim = new QPropertyAnimation(effect, "opacity"); + anim->setStartValue(1); + anim->setKeyValueAt(0.5,0); + anim->setEndValue(1); + anim->setDuration(400); + anim->start(); + } + +// soundManager->play(VOIP_SOUND_INCOMING_CALL); + + audioCaptureToggleButton->setToolTip(tr("Answer")); + + //TODO make a toaster and a sound for the incoming call + return; + } + + if (!outputAudioDevice) { + outputAudioDevice = AudioDeviceHelper::getDefaultOutputDevice(); + } + + if (!outputAudioProcessor) { + //start output audio device + outputAudioProcessor = new QtSpeex::SpeexOutputProcessor(); + if (inputAudioProcessor) { + connect(outputAudioProcessor, SIGNAL(playingFrame(QByteArray*)), inputAudioProcessor, SLOT(addEchoFrame(QByteArray*))); + } + outputAudioProcessor->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + outputAudioDevice->start(outputAudioProcessor); + } + + if (outputAudioDevice && outputAudioDevice->error() != QAudio::NoError) { + std::cerr << "Restarting output device. Error before reset " << outputAudioDevice->error() << " buffer size : " << outputAudioDevice->bufferSize() << std::endl; + outputAudioDevice->stop(); + outputAudioDevice->reset(); + if (outputAudioDevice->error() == QAudio::UnderrunError) + outputAudioDevice->setBufferSize(20); + outputAudioDevice->start(outputAudioProcessor); + } + outputAudioProcessor->putNetworkPacket(name, *array); + + //check the input device for errors + if (inputAudioDevice && inputAudioDevice->error() != QAudio::NoError) { + std::cerr << "Restarting input device. Error before reset " << inputAudioDevice->error() << std::endl; + inputAudioDevice->stop(); + inputAudioDevice->reset(); + inputAudioDevice->start(inputAudioProcessor); + } +} + +void VOIPChatWidgetHolder::sendAudioData() +{ + while(inputAudioProcessor && inputAudioProcessor->hasPendingPackets()) { + QByteArray qbarray = inputAudioProcessor->getNetworkPacket(); + RsVoipDataChunk chunk; + chunk.size = qbarray.size(); + chunk.data = (void*)qbarray.constData(); + rsVoip->sendVoipData(mChatWidget->getPeerId(),chunk); + } +} + +void VOIPChatWidgetHolder::updateStatus(int status) +{ + audioListenToggleButton->setEnabled(true); + audioCaptureToggleButton->setEnabled(true); + hangupButton->setEnabled(true); + + switch (status) { + case RS_STATUS_OFFLINE: + audioListenToggleButton->setEnabled(false); + audioCaptureToggleButton->setEnabled(false); + hangupButton->setEnabled(false); + break; + } +} diff --git a/plugins/VOIP/gui/VOIPChatWidgetHolder.h b/plugins/VOIP/gui/VOIPChatWidgetHolder.h new file mode 100644 index 000000000..56637a3d7 --- /dev/null +++ b/plugins/VOIP/gui/VOIPChatWidgetHolder.h @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +class QToolButton; +class QAudioInput; +class QAudioOutput; +class QVideoInputDevice ; +class QVideoOutputDevice ; +class VideoEncoder ; +class VideoDecoder ; + +#define VOIP_SOUND_INCOMING_CALL "VOIP_incoming_call" + +class VOIPChatWidgetHolder : public QObject, public ChatWidgetHolder +{ + Q_OBJECT + +public: + VOIPChatWidgetHolder(ChatWidget *chatWidget); + virtual ~VOIPChatWidgetHolder(); + + virtual void updateStatus(int status); + + void addAudioData(const QString name, QByteArray* array) ; + void addVideoData(const QString name, QByteArray* array) ; + +private slots: + void toggleAudioListen(); + void toggleAudioCapture(); + void toggleVideoCapture(); + void hangupCall() ; + +public slots: + void sendAudioData(); + +protected: + // Audio input/output + QAudioInput* inputAudioDevice; + QAudioOutput* outputAudioDevice; + + QtSpeex::SpeexInputProcessor* inputAudioProcessor; + QtSpeex::SpeexOutputProcessor* outputAudioProcessor; + + // Video input/output + QVideoOutputDevice *outputVideoDevice; + QVideoOutputDevice *echoVideoDevice; + QVideoInputDevice *inputVideoDevice; + + QWidget *videoWidget ; // pointer to call show/hide + + VideoEncoder *inputVideoProcessor; + VideoDecoder *outputVideoProcessor; + + // Additional buttons to the chat bar + QToolButton *audioListenToggleButton ; + QToolButton *audioCaptureToggleButton ; + QToolButton *videoCaptureToggleButton ; + QToolButton *hangupButton ; +}; + diff --git a/plugins/VOIP/gui/VOIP_images.qrc b/plugins/VOIP/gui/VOIP_images.qrc index 57b524325..c61faafe3 100644 --- a/plugins/VOIP/gui/VOIP_images.qrc +++ b/plugins/VOIP/gui/VOIP_images.qrc @@ -8,6 +8,9 @@ images/call-start-22.png images/call-stop-22.png images/call-hold-22.png + images/camera-on.png + images/camera-off.png + images/video-icon-big.png diff --git a/plugins/VOIP/gui/VideoProcessor.cpp b/plugins/VOIP/gui/VideoProcessor.cpp new file mode 100644 index 000000000..7e9b8e951 --- /dev/null +++ b/plugins/VOIP/gui/VideoProcessor.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include + +#include "VideoProcessor.h" +#include "QVideoDevice.h" + +//bool VideoDecoder::getNextImage(QImage& image) +//{ +// if(_image_queue.empty()) +// return false ; +// +// image = _image_queue.front() ; +// _image_queue.pop_front() ; +// +// return true ; +//} + +bool VideoEncoder::addImage(const QImage& img) +{ + std::cerr << "VideoEncoder: adding image." << std::endl; + + encodeData(img) ; + + if(_echo_output_device != NULL) + _echo_output_device->showFrame(img) ; + + return true ; +} + +void VideoDecoder::receiveEncodedData(const unsigned char *data,uint32_t size) +{ + _output_device->showFrame(decodeData(data,size)) ; +} + +QImage JPEGVideoDecoder::decodeData(const unsigned char *encoded_image_data,uint32_t size) +{ + QByteArray qb((char*)encoded_image_data,size) ; + QImage image ; + if(image.loadFromData(qb)) + return image ; + else + return QImage() ; +} + +void JPEGVideoEncoder::encodeData(const QImage& image) +{ + QByteArray qb ; + + QBuffer buffer(&qb) ; + buffer.open(QIODevice::WriteOnly) ; + image.save(&buffer,"JPEG") ; + + //destination_decoder->receiveEncodedData((unsigned char *)qb.data(),qb.size()) ; + + std::cerr <<"sending encoded data. size = " << qb.size() << std::endl; +} + diff --git a/plugins/VOIP/gui/VideoProcessor.h b/plugins/VOIP/gui/VideoProcessor.h new file mode 100644 index 000000000..6254f3777 --- /dev/null +++ b/plugins/VOIP/gui/VideoProcessor.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +class QVideoOutputDevice ; + +// This class decodes video from a stream. It keeps a queue of +// decoded frame that needs to be retrieved using the getNextImage() method. +// +class VideoDecoder +{ + public: + VideoDecoder() { _output_device = NULL ;} + + // Gets the next image to be displayed. Once returned, the image should + // be cleared from the incoming queue. + // + void setDisplayTarget(QVideoOutputDevice *odev) { _output_device = odev ; } + + virtual void receiveEncodedData(const unsigned char *data,uint32_t size) ; + + private: + QVideoOutputDevice *_output_device ; + + std::list _image_queue ; + + // Incoming data is processed by a video codec and converted into images. + // + virtual QImage decodeData(const unsigned char *encoded_image,uint32_t encoded_image_size) = 0 ; + + // This buffer accumulated incoming encoded data, until a full packet is obtained, + // since the stream might not send images at once. When incoming images are decoded, the + // data is removed from the buffer. + // + unsigned char *buffer ; + uint32_t buffer_size ; +}; + +// This class encodes video using a video codec (possibly homemade, or based on existing codecs) +// and produces a data stream that is sent to the network transfer service (e.g. p3VoRs). +// +class VideoEncoder +{ + public: + VideoEncoder() { _echo_output_device = NULL ;} + + // Takes the next image to be encoded. + // + virtual bool addImage(const QImage& Image) ; + + protected: + //virtual bool sendEncodedData(unsigned char *mem,uint32_t size) = 0 ; + virtual void encodeData(const QImage& image) = 0 ; + + unsigned char *buffer ; + uint32_t buffer_size ; + + QVideoOutputDevice *_echo_output_device ; +}; + +// Now derive various image encoding/decoding algorithms. +// + +class JPEGVideoDecoder: public VideoDecoder +{ + protected: + virtual QImage decodeData(const unsigned char *encoded_image,uint32_t encoded_image_size) ; +}; + +class JPEGVideoEncoder: public VideoEncoder +{ + public: + JPEGVideoEncoder() {} + + protected: + virtual void encodeData(const QImage& Image) ; +}; + diff --git a/plugins/VOIP/gui/images/camera-off.png b/plugins/VOIP/gui/images/camera-off.png new file mode 100644 index 0000000000000000000000000000000000000000..c1721d4922a7861a8e187cd7111032149e9ebbdf GIT binary patch literal 3476 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 literal 0 HcmV?d00001 diff --git a/plugins/VOIP/gui/images/camera-on.png b/plugins/VOIP/gui/images/camera-on.png new file mode 100644 index 0000000000000000000000000000000000000000..e6c8bd9ff70f49e15b42e6d025ffb570a9fdbc4a GIT binary patch literal 4092 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 z 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 literal 0 HcmV?d00001