sound player

This commit is contained in:
Gaelle Braud 2025-06-11 15:00:06 +02:00
parent 6ff3cc0ae7
commit 6aadc2f292
28 changed files with 1085 additions and 133 deletions

2
.gitmodules vendored
View file

@ -3,4 +3,4 @@ path = external/linphone-sdk
url = https://gitlab.linphone.org/BC/public/linphone-sdk.git
[submodule "external/qtkeychain"]
path = external/qtkeychain
url = https://github.com/frankosterfeld/qtkeychain.git
url = https://gitlab.linphone.org/BC/public/external/qtkeychain.git

View file

@ -206,7 +206,7 @@ set(ENABLE_CSHARP_WRAPPER OFF CACHE BOOL "Build the CSharp wrapper for Liblinpho
set(ENABLE_THEORA OFF)
set(ENABLE_QT_GL ${ENABLE_VIDEO})
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Core5Compat)
if(NOT Qt6_FOUND)
message(FATAL_ERROR "Minimum supported Qt6!")

View file

@ -84,6 +84,7 @@
#include "core/search/MagicSearchProxy.hpp"
#include "core/setting/SettingsCore.hpp"
#include "core/singleapplication/singleapplication.h"
#include "core/sound-player/SoundPlayerGui.hpp"
#include "core/timezone/TimeZoneProxy.hpp"
#include "core/translator/DefaultTranslatorCore.hpp"
#include "core/variant/VariantList.hpp"
@ -684,6 +685,7 @@ void App::initCppInterfaces() {
qmlRegisterType<CameraGui>(Constants::MainQmlUri, 1, 0, "CameraGui");
qmlRegisterType<FPSCounter>(Constants::MainQmlUri, 1, 0, "FPSCounter");
qmlRegisterType<EmojiModel>(Constants::MainQmlUri, 1, 0, "EmojiModel");
qmlRegisterType<SoundPlayerGui>(Constants::MainQmlUri, 1, 0, "SoundPlayerGui");
qmlRegisterType<TimeZoneProxy>(Constants::MainQmlUri, 1, 0, "TimeZoneProxy");

View file

@ -83,6 +83,9 @@ list(APPEND _LINPHONEAPP_SOURCES
core/screen/ScreenList.cpp
core/screen/ScreenProxy.cpp
core/sound-player/SoundPlayerCore.cpp
core/sound-player/SoundPlayerGui.cpp
core/videoSource/VideoSourceDescriptorCore.cpp
core/videoSource/VideoSourceDescriptorGui.cpp

View file

@ -0,0 +1,218 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QTimer>
#include <QtDebug>
#include "core/App.hpp"
#include "core/setting/SettingsCore.hpp"
#include "tool/Utils.hpp"
#include "SoundPlayerCore.hpp"
DEFINE_ABSTRACT_OBJECT(SoundPlayerCore)
// =============================================================================
using namespace std;
namespace {
int ForceCloseTimerInterval = 20;
}
// -----------------------------------------------------------------------------
QSharedPointer<SoundPlayerCore> SoundPlayerCore::create() {
auto sharedPointer = QSharedPointer<SoundPlayerCore>(new SoundPlayerCore(), &QObject::deleteLater);
sharedPointer->setSelf(sharedPointer);
sharedPointer->moveToThread(App::getInstance()->thread());
return sharedPointer;
}
SoundPlayerCore::SoundPlayerCore(QObject *parent) {
// connect(mForceCloseTimer, &QTimer::timeout, this, &SoundPlayerCore::handleEof);
}
SoundPlayerCore::~SoundPlayerCore() {
// mForceCloseTimer->stop();
}
void SoundPlayerCore::setSelf(QSharedPointer<SoundPlayerCore> me) {
auto settingsModel = SettingsModel::getInstance();
auto coreModel = CoreModel::getInstance();
mCoreModelConnection = SafeConnection<SoundPlayerCore, CoreModel>::create(me, coreModel);
mSettingsModelConnection = SafeConnection<SoundPlayerCore, SettingsModel>::create(me, settingsModel);
mSettingsModelConnection->makeConnectToModel(&SettingsModel::ringerDeviceChanged, [this, me] {
if (mSoundPlayerModel) mSoundPlayerModel->stop(true);
else mSettingsModelConnection->invokeToCore([this, me] { buildInternalPlayer(me); });
});
mCoreModelConnection->invokeToModel([this, me, settingsModel, coreModel] { buildInternalPlayer(me); });
// mCoreModelConnection->makeConnectToCore(&SoundPlayerCore::sourceChanged, [this, me] {
// mCoreModelConnection->invokeToModel([this, me] { buildInternalPlayer(me); });
// });
}
// -----------------------------------------------------------------------------
void SoundPlayerCore::buildInternalPlayer(QSharedPointer<SoundPlayerCore> me) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto coreModel = CoreModel::getInstance();
auto settingsModel = SettingsModel::getInstance();
if (mSoundPlayerModelConnection) mSoundPlayerModelConnection->disconnect();
auto player = coreModel->getCore()->createLocalPlayer(
Utils::appStringToCoreString(mIsRinger ? settingsModel->getRingerDevice()["display_name"].toString()
: settingsModel->getPlaybackDevice()["display_name"].toString()),
"", nullptr);
mHasVideo = player->getIsVideoAvailable();
mDuration = player->getDuration();
mSoundPlayerModel = Utils::makeQObject_ptr<SoundPlayerModel>(player);
mSoundPlayerModel->setSelf(mSoundPlayerModel);
mSoundPlayerModelConnection = SafeConnection<SoundPlayerCore, SoundPlayerModel>::create(me, mSoundPlayerModel);
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lStop, [this](bool force) {
mSoundPlayerModelConnection->invokeToModel([this, force] {
if (mPlaybackState == LinphoneEnums::PlaybackState::StoppedState && !force) return;
mSoundPlayerModel->stop(force);
});
});
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::stopped, [this, me](bool force) {
if (force) buildInternalPlayer(me);
});
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lPause, [this]() {
mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->pause(); });
});
mSoundPlayerModelConnection->makeConnectToModel(
&SoundPlayerModel::playbackStateChanged, [this](LinphoneEnums::PlaybackState state) {
mSoundPlayerModelConnection->invokeToCore([this, state] { setPlaybackState(state); });
});
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lOpen, [this]() {
mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->open(mSource); });
});
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lPlay, [this]() {
mSoundPlayerModelConnection->invokeToModel([this] { mSoundPlayerModel->play(mSource); });
});
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lSeek, [this](int offset) {
mSoundPlayerModelConnection->invokeToModel([this, offset] { mSoundPlayerModel->seek(offset); });
});
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::positionChanged, [this](int pos) {
mSoundPlayerModelConnection->invokeToCore([this, pos] { setPosition(pos); });
});
mSoundPlayerModelConnection->makeConnectToCore(&SoundPlayerCore::lRefreshPosition, [this]() {
mSoundPlayerModelConnection->invokeToModel([this] {
auto pos = mSoundPlayerModel->getPosition();
mSoundPlayerModelConnection->invokeToModel([this, pos] { setPosition(pos); });
});
});
mSoundPlayerModelConnection->makeConnectToModel(&SoundPlayerModel::eofReached,
[this](const shared_ptr<linphone::Player> &player) {
mSoundPlayerModelConnection->invokeToCore([this] {
mForceClose = true;
handleEof();
});
});
}
// -----------------------------------------------------------------------------
int SoundPlayerCore::getPosition() const {
return mPosition;
}
void SoundPlayerCore::setPosition(int position) {
if (mPosition != position) {
mPosition = position;
emit positionChanged();
}
}
bool SoundPlayerCore::hasVideo() const {
return mHasVideo;
}
void SoundPlayerCore::handleEof() {
if (mForceClose) {
mForceClose = false;
lStop();
}
}
void SoundPlayerCore::setError(const QString &message) {
qWarning() << message;
mError = message;
emit errorChanged(message);
}
QString SoundPlayerCore::getSource() const {
return mSource;
}
void SoundPlayerCore::setSource(const QString &source) {
if (source == mSource) return;
lStop(true);
mSource = source;
emit sourceChanged(source);
}
LinphoneEnums::PlaybackState SoundPlayerCore::getPlaybackState() const {
return mPlaybackState;
}
void SoundPlayerCore::setPlaybackState(LinphoneEnums::PlaybackState playbackState) {
if (mPlaybackState != playbackState) {
mPlaybackState = playbackState;
emit playbackStateChanged(playbackState);
}
// switch (playbackState) {
// case PlayingState:
// lPlay();
// break;
// case PausedState:
// lPause();
// break;
// case StoppedState:
// lStop();
// break;
// case ErrorState:
// break;
// }
}
// -----------------------------------------------------------------------------
int SoundPlayerCore::getDuration() const {
return mDuration;
}
QDateTime SoundPlayerCore::getCreationDateTime() const {
QFileInfo fileInfo(mSource);
QDateTime creationDate = fileInfo.birthTime();
return creationDate.isValid() ? creationDate : fileInfo.lastModified();
}
QString SoundPlayerCore::getBaseName() const {
return QFileInfo(mSource).baseName();
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SOUND_PLAYER_MODEL_H_
#define SOUND_PLAYER_MODEL_H_
#include "model/core/CoreModel.hpp"
#include "model/setting/SettingsModel.hpp"
#include "model/sound-player/SoundPlayerModel.hpp"
#include "tool/AbstractObject.hpp"
#include "tool/thread/SafeConnection.hpp"
#include <QMutex>
#include <QObject>
#include <linphone++/linphone.hh>
// =============================================================================
class QTimer;
class SoundPlayerCore : public QObject, public AbstractObject {
Q_OBJECT
Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QString baseName READ getBaseName NOTIFY sourceChanged)
Q_PROPERTY(LinphoneEnums::PlaybackState playbackState READ getPlaybackState WRITE setPlaybackState NOTIFY
playbackStateChanged)
Q_PROPERTY(int duration READ getDuration NOTIFY sourceChanged)
Q_PROPERTY(int position READ getPosition WRITE setPosition NOTIFY positionChanged)
Q_PROPERTY(bool isRinger MEMBER mIsRinger)
Q_PROPERTY(QDateTime creationDateTime READ getCreationDateTime NOTIFY sourceChanged)
public:
static QSharedPointer<SoundPlayerCore> create();
SoundPlayerCore(QObject *parent = Q_NULLPTR);
~SoundPlayerCore();
void setSelf(QSharedPointer<SoundPlayerCore> me);
int getPosition() const;
void setPosition(int position);
bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope.
int getDuration() const;
QDateTime getCreationDateTime() const;
QString getBaseName() const;
QString getSource() const;
void setSource(const QString &source);
LinphoneEnums::PlaybackState getPlaybackState() const;
void setPlaybackState(LinphoneEnums::PlaybackState playbackState);
signals:
bool lOpen();
void lPause();
bool lPlay();
void lStop(bool force = false);
void lSeek(int offset);
void lRefreshPosition();
void paused();
void playing();
void stopped();
void errorChanged(QString error);
void sourceChanged(const QString &source);
void playbackStateChanged(LinphoneEnums::PlaybackState playbackState);
void positionChanged();
private:
DECLARE_ABSTRACT_OBJECT
void buildInternalPlayer(QSharedPointer<SoundPlayerCore> me);
void handleEof();
void setError(const QString &message);
QString mSource;
LinphoneEnums::PlaybackState mPlaybackState = LinphoneEnums::PlaybackState::StoppedState;
bool mIsRinger = false;
bool mHasVideo = false;
int mPosition = 0;
int mDuration = 0;
QString mError;
bool mForceClose = false;
QMutex mForceCloseMutex;
QTimer *mForceCloseTimer = nullptr;
std::shared_ptr<SoundPlayerModel> mSoundPlayerModel;
QSharedPointer<SafeConnection<SoundPlayerCore, SoundPlayerModel>> mSoundPlayerModelConnection;
QSharedPointer<SafeConnection<SoundPlayerCore, CoreModel>> mCoreModelConnection;
QSharedPointer<SafeConnection<SoundPlayerCore, SettingsModel>> mSettingsModelConnection;
};
#endif // SOUND_PLAYER_MODEL_H_

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SoundPlayerGui.hpp"
#include "core/App.hpp"
DEFINE_ABSTRACT_OBJECT(SoundPlayerGui)
SoundPlayerGui::SoundPlayerGui(QObject *parent) : QObject(parent) {
mustBeInMainThread(getClassName());
mCore = SoundPlayerCore::create();
if (mCore) connect(mCore.get(), &SoundPlayerCore::sourceChanged, this, &SoundPlayerGui::sourceChanged);
if (mCore) connect(mCore.get(), &SoundPlayerCore::stopped, this, &SoundPlayerGui::stopped);
if (mCore) connect(mCore.get(), &SoundPlayerCore::positionChanged, this, &SoundPlayerGui::positionChanged);
}
SoundPlayerGui::SoundPlayerGui(QSharedPointer<SoundPlayerCore> core) {
App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership);
mCore = core;
if (isInLinphoneThread()) moveToThread(App::getInstance()->thread());
}
SoundPlayerGui::~SoundPlayerGui() {
mustBeInMainThread("~" + getClassName());
}
SoundPlayerCore *SoundPlayerGui::getCore() const {
return mCore.get();
}
QString SoundPlayerGui::getSource() const {
return mCore ? mCore->getSource() : QString();
}
void SoundPlayerGui::setSource(QString source) {
if (mCore) mCore->setSource(source);
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SOUND_PLAYER_GUI_H_
#define SOUND_PLAYER_GUI_H_
#include "SoundPlayerCore.hpp"
#include <QObject>
#include <QSharedPointer>
class SoundPlayerGui : public QObject, public AbstractObject {
Q_OBJECT
Q_PROPERTY(SoundPlayerCore *core READ getCore CONSTANT)
Q_PROPERTY(QString source READ getSource WRITE setSource NOTIFY sourceChanged)
public:
SoundPlayerGui(QObject *parent = nullptr);
SoundPlayerGui(QSharedPointer<SoundPlayerCore> core);
~SoundPlayerGui();
SoundPlayerCore *getCore() const;
QString getSource() const;
void setSource(QString source);
QSharedPointer<SoundPlayerCore> mCore;
signals:
void sourceChanged();
void stopped();
void positionChanged();
private:
DECLARE_ABSTRACT_OBJECT
};
#endif

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.1875 3.375V14.625C15.1875 14.9234 15.069 15.2095 14.858 15.4205C14.647 15.6315 14.3609 15.75 14.0625 15.75H11.25C10.9516 15.75 10.6655 15.6315 10.4545 15.4205C10.2435 15.2095 10.125 14.9234 10.125 14.625V3.375C10.125 3.07663 10.2435 2.79048 10.4545 2.5795C10.6655 2.36853 10.9516 2.25 11.25 2.25H14.0625C14.3609 2.25 14.647 2.36853 14.858 2.5795C15.069 2.79048 15.1875 3.07663 15.1875 3.375ZM6.75 2.25H3.9375C3.63913 2.25 3.35298 2.36853 3.142 2.5795C2.93103 2.79048 2.8125 3.07663 2.8125 3.375V14.625C2.8125 14.9234 2.93103 15.2095 3.142 15.4205C3.35298 15.6315 3.63913 15.75 3.9375 15.75H6.75C7.04837 15.75 7.33452 15.6315 7.5455 15.4205C7.75647 15.2095 7.875 14.9234 7.875 14.625V3.375C7.875 3.07663 7.75647 2.79048 7.5455 2.5795C7.33452 2.36853 7.04837 2.25 6.75 2.25Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 907 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 5.38818V18.6118C19.9995 18.9798 19.8531 19.3327 19.5929 19.5929C19.3327 19.8531 18.9798 19.9995 18.6118 20H5.38818C5.02016 19.9995 4.66735 19.8531 4.40712 19.5929C4.14689 19.3327 4.00048 18.9798 4 18.6118V5.38818C4.00048 5.02016 4.14689 4.66735 4.40712 4.40712C4.66735 4.14689 5.02016 4.00048 5.38818 4H18.6118C18.9798 4.00048 19.3327 4.14689 19.5929 4.40712C19.8531 4.66735 19.9995 5.02016 20 5.38818Z" fill="#343330"/>
</svg>

After

Width:  |  Height:  |  Size: 537 B

View file

@ -90,7 +90,7 @@ int main(int argc, char *argv[]) {
result = app->exec();
} while (result == (int)App::StatusCode::gRestartCode);
QString message = "[Main] Exiting app with the code : " + QString::number(result);
if (result) qInfo() << message;
if (!result) qInfo() << message;
else qWarning() << message;
app->clean();
app = nullptr;

View file

@ -41,6 +41,7 @@ list(APPEND _LINPHONEAPP_SOURCES
model/setting/SettingsModel.cpp
model/setting/MediastreamerUtils.cpp
model/sound-player/SoundPlayerModel.cpp
model/tool/ToolModel.cpp
model/tool/VfsUtils.cpp

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QTimer>
#include <QtDebug>
#include "model/core/CoreModel.hpp"
#include "tool/Utils.hpp"
#include "SoundPlayerModel.hpp"
DEFINE_ABSTRACT_OBJECT(SoundPlayerModel)
// =============================================================================
using namespace std;
namespace {
int ForceCloseTimerInterval = 20;
}
class SoundPlayerModel::Handlers : public linphone::PlayerListener {
public:
Handlers(SoundPlayerModel *soundPlayerModel) {
mSoundPlayerModel = soundPlayerModel;
}
private:
SoundPlayerModel *mSoundPlayerModel;
};
// -----------------------------------------------------------------------------
SoundPlayerModel::SoundPlayerModel(const std::shared_ptr<linphone::Player> &player, QObject *parent)
: ::Listener<linphone::Player, linphone::PlayerListener>(player, parent) {
mustBeInLinphoneThread(getClassName());
}
SoundPlayerModel::~SoundPlayerModel() {
if (mMonitor) mMonitor->close();
}
// -----------------------------------------------------------------------------
void SoundPlayerModel::pause() {
if (mMonitor->pause()) {
//: Unable to pause
emit errorChanged("sound_player_pause_error");
emit playbackStateChanged(LinphoneEnums::PlaybackState::ErrorState);
return;
}
emit paused();
emit playbackStateChanged(LinphoneEnums::PlaybackState::PausedState);
}
bool SoundPlayerModel::open(QString source) {
mMonitor->open(Utils::appStringToCoreString(source));
emit open();
return true;
// }
// return false;
}
bool SoundPlayerModel::play(QString source) {
if (source == "") return false;
if (!open(source)) {
qWarning() << QStringLiteral("Unable to open: `%1`").arg(source);
//: Unable to open: `%1`
emit errorChanged(QString("sound_player_open_error").arg(source));
return false;
}
if (mMonitor->start()) {
//: Unable to play %1
emit errorChanged(QString("sound_player_play_error").arg(source));
emit playbackStateChanged(LinphoneEnums::PlaybackState::ErrorState);
return false;
}
emit playing();
emit playbackStateChanged(LinphoneEnums::PlaybackState::PlayingState);
return true;
}
// -----------------------------------------------------------------------------
void SoundPlayerModel::seek(int offset) {
mMonitor->seek(offset);
emit positionChanged(mMonitor->getCurrentPosition());
}
// -----------------------------------------------------------------------------
int SoundPlayerModel::getPosition() const {
return mMonitor->getCurrentPosition();
}
bool SoundPlayerModel::hasVideo() const {
return mMonitor->getIsVideoAvailable();
}
void SoundPlayerModel::stop(bool force) {
if (mMonitor) mMonitor->close();
emit stopped(force);
emit playbackStateChanged(LinphoneEnums::PlaybackState::StoppedState);
}
// -----------------------------------------------------------------------------
int SoundPlayerModel::getDuration() const {
return mMonitor->getDuration();
}
/*------------------------------------------------------------------*/
void SoundPlayerModel::onEofReached(const shared_ptr<linphone::Player> &player) {
if (player == mMonitor) emit eofReached(player);
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SOUND_PLAYER_H_
#define SOUND_PLAYER_H_
#include "tool/AbstractObject.hpp"
#include <memory>
#include <QMutex>
#include <QObject>
// =============================================================================
class QTimer;
class SoundPlayerModel : public ::Listener<linphone::Player, linphone::PlayerListener>,
public linphone::PlayerListener,
public AbstractObject {
class Handlers;
Q_OBJECT
public:
SoundPlayerModel(const std::shared_ptr<linphone::Player> &player, QObject *parent = nullptr);
~SoundPlayerModel();
bool open(QString source);
void pause();
bool play(QString source);
void stop(bool force = false);
void seek(int offset);
int getPosition() const;
bool hasVideo() const; // Call it after playing a video because the detection is not outside this scope.
int getDuration() const;
void handleEof();
void setError(const QString &message);
signals:
void sourceChanged(const QString &source);
void paused();
void open();
void playing();
void stopped(bool force);
void positionChanged(int position);
void errorChanged(QString error);
void playbackStateChanged(LinphoneEnums::PlaybackState playbackState);
void eofReached(const std::shared_ptr<linphone::Player> &player);
private:
DECLARE_ABSTRACT_OBJECT
void onEofReached(const std::shared_ptr<linphone::Player> &player);
};
#endif // SOUND_PLAYER_H_

View file

@ -41,7 +41,8 @@ void VideoSourceDescriptorModel::setScreenSharingDisplay(int index) {
void VideoSourceDescriptorModel::setScreenSharingWindow(void *window) { // Get data from DesktopTools.
if (!mDesc) mDesc = linphone::Factory::get()->createVideoSourceDescriptor();
else if (getVideoSourceType() == LinphoneEnums::VideoSourceScreenSharingTypeWindow && window == getScreenSharing())
else if (getVideoSourceType() == LinphoneEnums::VideoSourceScreenSharingType::VideoSourceScreenSharingTypeWindow &&
window == getScreenSharing())
return;
mDesc->setScreenSharing(linphone::VideoSourceScreenSharingType::Window, window);
emit videoDescriptorChanged();

View file

@ -24,6 +24,7 @@ list(APPEND _LINPHONEAPP_SOURCES
tool/file/FileDownloader.cpp
tool/file/FileExtractor.cpp
tool/file/TemporaryFile.cpp
tool/ui/DashRectangle.cpp

View file

@ -39,6 +39,7 @@ void LinphoneEnums::registerMetaTypes() {
qRegisterMetaType<LinphoneEnums::FriendCapability>();
qRegisterMetaType<LinphoneEnums::MediaEncryption>();
qRegisterMetaType<LinphoneEnums::ParticipantDeviceState>();
qRegisterMetaType<LinphoneEnums::PlaybackState>();
qRegisterMetaType<LinphoneEnums::RecorderState>();
qRegisterMetaType<LinphoneEnums::RegistrationState>();
qRegisterMetaType<LinphoneEnums::TunnelMode>();

View file

@ -383,7 +383,7 @@ Q_ENUM_NS(AccountManagerServicesRequestType)
// &type); LinphoneEnums::AccountManagerServicesRequestType fromLinphone(const
// linphone::AccountManagerServicesRequest::Type &type);
enum VideoSourceScreenSharingType {
enum class VideoSourceScreenSharingType {
VideoSourceScreenSharingTypeArea = int(linphone::VideoSourceScreenSharingType::Area),
VideoSourceScreenSharingTypeDisplay = int(linphone::VideoSourceScreenSharingType::Display),
VideoSourceScreenSharingTypeWindow = int(linphone::VideoSourceScreenSharingType::Window)
@ -393,6 +393,9 @@ Q_ENUM_NS(VideoSourceScreenSharingType)
linphone::VideoSourceScreenSharingType toLinphone(const LinphoneEnums::VideoSourceScreenSharingType &type);
LinphoneEnums::VideoSourceScreenSharingType fromLinphone(const linphone::VideoSourceScreenSharingType &type);
enum class PlaybackState { PlayingState = 0, PausedState = 1, StoppedState = 2, ErrorState = 3 };
Q_ENUM_NS(PlaybackState);
} // namespace LinphoneEnums
/*
Q_DECLARE_METATYPE(LinphoneEnums::CallState)

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include "TemporaryFile.hpp"
#include "core/chat/message/content/ChatMessageContentCore.hpp"
#include "model/core/CoreModel.hpp"
#include "model/setting/SettingsModel.hpp"
#include "tool/Utils.hpp"
#include <linphone++/linphone.hh>
// =============================================================================
using namespace std;
DEFINE_ABSTRACT_OBJECT(TemporaryFile)
TemporaryFile::TemporaryFile(QObject *parent) : QObject(parent) {
}
TemporaryFile::~TemporaryFile() {
deleteFile();
}
void TemporaryFile::createFileFromContent(std::shared_ptr<linphone::Content> content, const bool &exportPlainFile) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
if (content) {
QString filePath;
if (exportPlainFile || (SettingsModel::getInstance()->getVfsEncrypted() && content->isFileEncrypted()))
filePath = Utils::coreStringToAppString(content->exportPlainFile());
bool toDelete = true;
if (filePath.isEmpty()) {
filePath = Utils::coreStringToAppString(content->getFilePath());
toDelete = false;
if (content->isFileEncrypted()) // filePath was empty while the file is encrypted : it couldn't be decoded.
setIsReadable(false);
else setIsReadable(true);
} else setIsReadable(true);
setFilePath(filePath, toDelete);
}
}
void TemporaryFile::createFile(const QString &openFilePath, const bool &exportPlainFile) {
createFileFromContent(linphone::Factory::get()->createContentFromFile(Utils::appStringToCoreString(openFilePath)),
exportPlainFile);
}
QString TemporaryFile::getFilePath() const {
return mFilePath;
}
bool TemporaryFile::isReadable() const {
return mIsReadable;
}
void TemporaryFile::setFilePath(const QString &path, const bool &toDelete) {
if (path != mFilePath) {
deleteFile();
mFilePath = path;
mDeleteFile = toDelete;
emit filePathChanged();
}
}
void TemporaryFile::setIsReadable(const bool &isReadable) {
if (isReadable != mIsReadable) {
mIsReadable = isReadable;
emit isReadableChanged();
}
}
void TemporaryFile::deleteFile() {
if (mDeleteFile && !mFilePath.isEmpty()) QFile::remove(mFilePath);
mFilePath = "";
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TEMPORARY_FILE_H_
#define TEMPORARY_FILE_H_
#include "tool/AbstractObject.hpp"
#include <QFile>
#include <linphone++/linphone.hh>
// =============================================================================
class ContentModel;
class TemporaryFile : public QObject, public AbstractObject {
Q_OBJECT
public:
TemporaryFile(QObject *parent = nullptr);
~TemporaryFile();
Q_PROPERTY(QString filePath READ getFilePath NOTIFY
filePathChanged) // not changeable from QML as it comes from a ContentModel
Q_PROPERTY(bool isReadable READ isReadable NOTIFY isReadableChanged)
void createFileFromContent(std::shared_ptr<linphone::Content> content, const bool &exportPlainFile = true);
Q_INVOKABLE void createFile(const QString &filePath, const bool &exportPlainFile = true);
QString getFilePath() const;
bool isReadable() const;
void setFilePath(const QString &path, const bool &toDelete);
void setIsReadable(const bool &isReadable);
void deleteFile();
signals:
void filePathChanged();
void isReadableChanged();
private:
DECLARE_ABSTRACT_OBJECT
QString mFilePath;
bool mDeleteFile = false;
bool mIsReadable = false;
};
#endif

View file

@ -46,6 +46,7 @@ list(APPEND _LINPHONEAPP_QML_FILES
view/Control/Display/Flickable.qml
view/Control/Display/GradientRectangle.qml
view/Control/Display/TemporaryText.qml
view/Control/Display/MediaProgressBar.qml
view/Control/Display/ProgressBar.qml
view/Control/Display/RoundedPane.qml
view/Control/Display/RoundProgressBar.qml

View file

@ -11,117 +11,81 @@ Loader{
id: mainItem
property ChatMessageContentGui chatMessageContentGui
property int availableWidth : parent.width
property int width: active ? Math.max(availableWidth - ChatAudioMessageStyle.emptySpace, ChatAudioMessageStyle.minWidth) : 0
property int fitHeight: active ? 60 : 0
property font customFont : SettingsModel.textMessageFont
property bool isActive: active
// property string filePath : tempFile.filePath
property string filePath : tempFile.filePath
active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording
active: chatMessageContentGui && chatMessageContentGui.core.isVoiceRecording()
// onChatMessageContentGuiChanged: if(chatMessageContentGui){
// tempFile.createFileFromContentModel(chatMessageContentGui, false);
// }
onChatMessageContentGuiChanged: if(chatMessageContentGui){
tempFile.createFileFromContentModel(chatMessageContentGui, false);
}
TemporaryFile {
id: tempFile
}
// TemporaryFile {
// id: tempFile
// }
sourceComponent: Item {
id: loadedItem
property bool isPlaying : vocalPlayer.item && vocalPlayer.item.playbackState === SoundPlayer.PlayingState
property bool isPlaying : soundPlayerGui && soundPlayerGui.core.playbackState === LinphoneEnums.PlaybackState.PlayingState
onIsPlayingChanged: isPlaying ? mediaProgressBar.resume() : mediaProgressBar.stop()
width: availableWidth < 0 || availableWidth > mainItem.width ? mainItem.width : availableWidth
height: mainItem.fitHeight
width: mainItem.width
height: mainItem.height
clip: false
Loader {
id: vocalPlayer
active: false
SoundPlayerGui {
id: soundPlayerGui
property int duration: mainItem.chatMessageContentGui ? mainItem.chatMessageContentGui.core.fileDuration : core.duration
property int position: core.position
source: mainItem.chatMessageContentGui && mainItem.chatMessageContentGui.core.filePath
function play(){
if(!vocalPlayer.active)
vocalPlayer.active = true
else {
if(loadedItem.isPlaying){// Pause the play
vocalPlayer.item.pause()
soundPlayerGui.core.lPause()
}else{// Play the audio
vocalPlayer.item.play()
soundPlayerGui.core.lPlay()
}
}
}
sourceComponent: SoundPlayer {
source: mainItem.chatMessageContentGui && mainItem.filePath
onStopped: {
mediaProgressBar.value = 101
}
Component.onCompleted: {
play()// This will open the file and allow seeking
pause()
onPositionChanged: {
mediaProgressBar.progressPosition = position
mediaProgressBar.value = 100 * ( mediaProgressBar.progressPosition / duration)
}
onSourceChanged: if (source != "") {
// core.lPlay()// This will open the file and allow seeking
// core.lPause()
core.lOpen()
mediaProgressBar.value = 0
mediaProgressBar.refresh()
}
}
onStatusChanged: if (loader.status == Loader.Ready) play()
}
RowLayout{
id: lineLayout
anchors.fill: parent
spacing: 5
ActionButton{
id: playButton
Layout.preferredHeight: iconSize
Layout.preferredWidth: iconSize
Layout.rightMargin: 5
Layout.leftMargin: 15
Layout.alignment: Qt.AlignVCenter
isCustom: true
backgroundRadius: width
colorSet: (loadedItem.isPlaying ? ChatAudioMessageStyle.pauseAction
: ChatAudioMessageStyle.playAction)
onClicked:{
vocalPlayer.play()
}
}
Item{
Layout.fillHeight: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 10
Layout.topMargin: 10
Layout.bottomMargin: 10
MediaProgressBar{
id: mediaProgressBar
anchors.fill: parent
progressDuration: vocalPlayer.item ? vocalPlayer.item.duration : chatMessageContentGui.core.getFileDuration()
progressDuration: soundPlayerGui ? soundPlayerGui.duration : chatMessageContentGui.core.fileDuration
progressPosition: 0
value: 0
stopAtEnd: true
resetAtEnd: false
backgroundColor: ChatAudioMessageStyle.backgroundColor.color
colorSet: ChatAudioMessageStyle.progressionWave
function refresh(){
if( vocalPlayer.item){
progressPosition = vocalPlayer.item.getPosition()
value = 100 * ( progressPosition / vocalPlayer.item.duration)
if(soundPlayerGui){
soundPlayerGui.core.lRefreshPosition()
}
}
onEndReached:{
if(vocalPlayer.item)
vocalPlayer.item.stop()
if(soundPlayerGui)
soundPlayerGui.core.lStop()
}
onPlayStopButtonToggled: soundPlayerGui.play()
onRefreshPositionRequested: refresh()
onSeekRequested: if( vocalPlayer.item){
vocalPlayer.item.seek(ms)
progressPosition = vocalPlayer.item.getPosition()
value = 100 * (progressPosition / vocalPlayer.item.duration)
onSeekRequested: (ms) => {
if(soundPlayerGui) {
soundPlayerGui.core.lSeek(ms)
}
}
}
}
}
}

View file

@ -26,33 +26,23 @@ ColumnLayout {
property int padding: Math.round(10 * DefaultStyle.dp)
// VOICE MESSAGES
// ListView {
// id: messagesVoicesList
// width: parent.width-2*mainItem.padding
// visible: count > 0
// spacing: 0
// clip: false
// model: ChatMessageContentProxy {
// filterType: ChatMessageContentProxy.FilterContentType.Voice
// chatMessageGui: mainItem.chatMessageGui
// }
// height: contentHeight
// boundsBehavior: Flickable.StopAtBounds
// interactive: false
// function updateBestWidth(){
// var newWidth = mainItem.updateListBestWidth(messagesVoicesList)
// mainItem.voicesCount = newWidth[0]
// mainItem.voicesBestWidth = newWidth[1]
// }
// delegate: ChatAudioMessage{
// id: audioMessage
// contentModel: $modelData
// visible: contentModel
// z: 1
// Component.onCompleted: messagesVoicesList.updateBestWidth()
// }
// Component.onCompleted: messagesVoicesList.updateBestWidth
// }
Repeater {
id: messagesVoicesList
visible: mainItem.chatMessageGui.core.isVoiceRecording && count > 0
model: ChatMessageContentProxy{
filterType: ChatMessageContentProxy.FilterContentType.Voice
chatMessageGui: mainItem.chatMessageGui
}
delegate: ChatAudioContent {
// Layout.fillWidth: true
width: Math.round(269 * DefaultStyle.dp)
height: Math.round(48 * DefaultStyle.dp)
Layout.preferredHeight: height
chatMessageContentGui: modelData
// width: conferenceList.width
// onMouseEvent: (event) => mainItem.mouseEvent(event)
}
}
// CONFERENCE
Repeater {
id: conferenceList

View file

@ -0,0 +1,165 @@
import QtQuick
import QtQuick.Controls as Control
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Linphone
import UtilsCpp
import 'qrc:/qt/qml/Linphone/view/Style/buttonStyle.js' as ButtonStyle
// =============================================================================
ProgressBar {
id: mainItem
property bool stopAtEnd: true
property bool resetAtEnd: true
property int progressDuration // Max duration
property int progressPosition // Position of progress bar in [0 ; progressDuration]
property bool blockValueAtEnd: true
property bool recording: false
padding: 0
clip: true
function start(){
mainItem.value = 0
animationTest.start()
}
function resume(){
if(mainItem.value >= 100)
mainItem.value = 0
animationTest.start()
}
function stop(){
animationTest.stop()
}
signal playStopButtonToggled()
signal endReached()
signal refreshPositionRequested()
signal seekRequested(int ms)
Timer{
id: animationTest
repeat: true
onTriggered: mainItem.refreshPositionRequested()
interval: 5
}
to: 101
value: progressPosition * to / progressDuration
onValueChanged:{
if(value > 100) {
if( mainItem.stopAtEnd)
stop()
if(mainItem.resetAtEnd) {
mainItem.value = 0
progressPosition = 0
} else if(mainItem.blockValueAtEnd){
mainItem.value = 100// Stay at 100
progressPosition = progressDuration
}
console.log("end reached")
mainItem.endReached()
}
}
background: Item {
anchors.fill: parent
Rectangle {
id: backgroundArea
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#FF9E79" }
GradientStop { position: 1.0; color: "#FE5E00" }
}
radius: Math.round(70 * DefaultStyle.dp)
}
Rectangle {
id: mask
anchors.fill: parent
visible: false
radius: backgroundArea.radius
}
Item {
anchors.fill: parent
id: progressRectangle
visible: false
Rectangle {
color: DefaultStyle.grey_0
width: mainItem.barWidth
height: backgroundArea.height
opacity: 0.5
}
}
OpacityMask {
anchors.fill: progressRectangle
source: progressRectangle
maskSource: mask
}
MouseArea {
id: progression
anchors.fill: parent
onClicked: (mouse) => {
mainItem.seekRequested(mouse.x * mainItem.progressDuration/width)
}
}
}
contentItem: Item {
id: contentRect
RoundButton {
z: parent.z + 1
anchors.left: parent.left
anchors.leftMargin: Math.round(9 * DefaultStyle.dp)
anchors.verticalCenter: parent.verticalCenter
icon.width: Math.round(14 * DefaultStyle.dp)
icon.height: Math.round(14 * DefaultStyle.dp)
icon.source: animationTest.running
? mainItem.recording
? AppIcons.stopFill
: AppIcons.pauseFill
: AppIcons.playFill
onClicked: {
mainItem.playStopButtonToggled()
}
borderColor: "transparent"
style: ButtonStyle.secondary
}
Control.Control {
anchors.right: parent.right
anchors.rightMargin: Math.round(9 * DefaultStyle.dp)
anchors.verticalCenter: parent.verticalCenter
leftPadding: Math.round(18 * DefaultStyle.dp)
rightPadding: Math.round(18 * DefaultStyle.dp)
topPadding: Math.round(5 * DefaultStyle.dp)
bottomPadding: Math.round(5 * DefaultStyle.dp)
background: Rectangle {
anchors.fill: parent
color: DefaultStyle.grey_0
radius: Math.round(50 * DefaultStyle.dp)
}
contentItem: RowLayout {
spacing: mainItem.recording ? Math.round(5 * DefaultStyle.dp) : 0
EffectImage {
visible: mainItem.recording
colorizationColor: DefaultStyle.danger_500main
imageSource: AppIcons.recordFill
}
Text {
id: durationText
text: mainItem.progressPosition > 0 ? UtilsCpp.formatElapsedTime(mainItem.progressPosition / 1000 ) : UtilsCpp.formatElapsedTime(mainItem.progressDuration/1000)
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
}
}
}
}
}
}

View file

@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Controls.Basic as Control
import Linphone
ProgressBar {
Control.ProgressBar {
id: mainItem
padding: Math.round(3 * DefaultStyle.dp)

View file

@ -111,7 +111,6 @@ Control.Control {
Flickable {
id: sendingAreaFlickable
Layout.fillWidth: true
Layout.preferredWidth: parent.width - stackButton.width
Layout.preferredHeight: Math.min(Math.round(60 * DefaultStyle.dp), contentHeight)
Binding {
target: sendingAreaFlickable
@ -159,10 +158,11 @@ Control.Control {
}
}
}
StackLayout {
RowLayout {
id: stackButton
currentIndex: sendingTextArea.text.length === 0 ? 0 : 1
spacing: 0
BigButton {
visible: sendingTextArea.text.length === 0
style: ButtonStyle.noBackground
icon.source: AppIcons.microphone
onClicked: {
@ -170,6 +170,7 @@ Control.Control {
}
}
BigButton {
visible: sendingTextArea.text.length !== 0
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
onClicked: {

View file

@ -74,8 +74,10 @@ QtObject {
property string trustedMask: "image://internal/trusted-mask.svg"
property string avatar: "image://internal/randomAvatar.png"
property string pause: "image://internal/pause.svg"
property string pauseFill: "image://internal/pause-fill.svg"
property string play: "image://internal/play.svg"
property string playFill: "image://internal/play-fill.svg"
property string stopFill: "image://internal/stop-filled.svg"
property string filePdf: "image://internal/file-pdf.svg"
property string fileText: "image://internal/file-text.svg"
property string fileImage: "image://internal/file-image.svg"