From: APTX Date: Sun, 26 Feb 2017 12:44:11 +0000 (+0100) Subject: Initial Aniplayer3 commit X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=45897a6b3dbfc810f281e1f2994f33ff232b71c1;p=aniplayer.git Initial Aniplayer3 commit --- 45897a6b3dbfc810f281e1f2994f33ff232b71c1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f298019 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +.qmake.cache +tags +.DS_Store +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +*.qmake.stash + +# qtcreator generated files +*.pro.user +*.pro.user.* +*.autosave +*.files +*.creator +*.creator.* +*.config +*.includes + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.exp + +# MinGW generated files +*.Debug +*.Release + +# Directories to ignore +# --------------------- + +build +debug +release +lib/qtsingleapplication/lib +lib/qtsingleapplication/examples +lib/qtsingleapplication/doc +.tmp +qtc-gdbmacros +test-data + +# Binaries +# -------- +build/*.dll +build/*.lib +build/*.exe +build/*.so* + + diff --git a/aniplayer3.pro b/aniplayer3.pro new file mode 100644 index 0000000..821e32c --- /dev/null +++ b/aniplayer3.pro @@ -0,0 +1,5 @@ +TEMPLATE = subdirs + +SUBDIRS += \ + core \ + backendplugins diff --git a/backendplugins/backend_mpv/backend_mpv.json b/backendplugins/backend_mpv/backend_mpv.json new file mode 100644 index 0000000..e69de29 diff --git a/backendplugins/backend_mpv/backend_mpv.pro b/backendplugins/backend_mpv/backend_mpv.pro new file mode 100644 index 0000000..4cb0b3b --- /dev/null +++ b/backendplugins/backend_mpv/backend_mpv.pro @@ -0,0 +1,28 @@ +TARGET = backend_mpv +TEMPLATE = lib +include(../../core/core.pri) +include(../backendbuildconfig.pri) + +DEFINES += BACKEND_MPV_LIBRARY QT_DEPRECATED_WARNINGS + +SOURCES += \ + backendmpv.cpp + +HEADERS += \ + backendmpv.h \ + backend_mpv_global.h + +unix { + LIBS += $$system(pkg-config --libs mpv) +} +!unix { + LIBS += -lmpv-1 +} + +unix { + target.path = /usr/lib/aniplayer/backendplugins + INSTALLS += target +} + +DISTFILES += \ + backend_mpv.json diff --git a/backendplugins/backend_mpv/backend_mpv_global.h b/backendplugins/backend_mpv/backend_mpv_global.h new file mode 100644 index 0000000..f7f4b2d --- /dev/null +++ b/backendplugins/backend_mpv/backend_mpv_global.h @@ -0,0 +1,12 @@ +#ifndef BACKEND_MPV_GLOBAL_H +#define BACKEND_MPV_GLOBAL_H + +#include + +#if defined(BACKEND_MPV_LIBRARY) +# define BACKEND_MPVSHARED_EXPORT Q_DECL_EXPORT +#else +# define BACKEND_MPVSHARED_EXPORT Q_DECL_IMPORT +#endif + +#endif // BACKEND_MPV_GLOBAL_H diff --git a/backendplugins/backend_mpv/backendmpv.cpp b/backendplugins/backend_mpv/backendmpv.cpp new file mode 100644 index 0000000..2181fc0 --- /dev/null +++ b/backendplugins/backend_mpv/backendmpv.cpp @@ -0,0 +1,319 @@ +#include "backendmpv.h" + +#include +#include +#include + +#include +#include + +#include + +Q_LOGGING_CATEGORY(mpvBackend, "MPV") +Q_LOGGING_CATEGORY(mpvLog, "MPV Log") + +BackendMpv::BackendMpv() { +#ifdef Q_OS_UNIX + setlocale(LC_NUMERIC, "C"); +#endif +} + +BackendMpv::~BackendMpv() {} + +bool BackendMpv::initialize(PlayerPluginInterface *playerInterface) { + qCDebug(mpvBackend) << "Initialize"; + m_player = playerInterface; + m_handle = mpv_create(); + + qCDebug(mpvBackend()).nospace() + << "Client API version: " << (mpv_client_api_version() >> 16) << '.' + << (mpv_client_api_version() & ~(~0u << 16)); + + // mpv_set_option(handle, "wid", MPV_FORMAT_INT64, &wid); + mpv_set_option_string(m_handle, "vo", "opengl-cb"); + + int error = mpv_initialize(m_handle); + if (error) { + qCDebug(mpvBackend) << "Error initializing mpv" << mpv_error_string(error); + return false; + } + + mpv_set_wakeup_callback(m_handle, mpvWakeupCb, this); + qCDebug(mpvBackend) << "register pause" + << mpv_observe_property(m_handle, 0, "pause", + MPV_FORMAT_FLAG); + qCDebug(mpvBackend) << "register duration" + << mpv_observe_property(m_handle, 0, "duration", + MPV_FORMAT_DOUBLE); + qCDebug(mpvBackend) << "register playback-time" + << mpv_observe_property(m_handle, 0, "playback-time", + MPV_FORMAT_DOUBLE); + qCDebug(mpvBackend) << "register ao-volume" + << mpv_observe_property(m_handle, 0, "ao-volume", + MPV_FORMAT_DOUBLE); + + qCDebug(mpvBackend) << "request log messages" + << mpv_request_log_messages(m_handle, "info"); + return !error; +} + +void BackendMpv::deinitialize() { + qCDebug(mpvBackend) << "Deinitialize"; + mpv_terminate_destroy(m_handle); +} + +VideoRendererBase *BackendMpv::createRenderer(VideoUpdateInterface *vui) { + qCDebug(mpvBackend, "BackendMpv::createRenderer"); + return new VideoRendererMpv(m_handle, vui); +} + +bool BackendMpv::open(const QUrl &source) { + qCDebug(mpvBackend) << "Opening " << source; + const QByteArray tmp = source.toLocalFile().toUtf8(); + + const char *args[] = {"loadfile", tmp.constData(), NULL}; + mpv_command_async(m_handle, 1, args); + pause(); + + return true; +} + +void BackendMpv::play() { + qCDebug(mpvBackend) << "Play"; + int f = 0; + mpv_set_property(m_handle, "pause", MPV_FORMAT_FLAG, &f); +} + +void BackendMpv::pause() { + qCDebug(mpvBackend) << "Pause"; + int f = 1; + mpv_set_property(m_handle, "pause", MPV_FORMAT_FLAG, &f); +} + +void BackendMpv::stop() { qCDebug(mpvBackend) << "Stop"; } + +void BackendMpv::seek(TimeStamp pos) { + mpv_set_property(m_handle, "playback-time", MPV_FORMAT_DOUBLE, &pos); +} + +void BackendMpv::setVolume(BackendPluginBase::Volume volume) { + double percantageVolume = volume * 100; + int error = mpv_set_property(m_handle, "ao-volume", MPV_FORMAT_DOUBLE, + &percantageVolume); + if (error) { + qCDebug(mpvBackend) + << "Audio output not yet ready, setting volume at a later time"; + m_volumeToSet = volume; + m_player->playbackVolumeChanged(volume); + } else { + qCDebug(mpvBackend) << "Audio volume set"; + m_volumeToSet = -1; + } +} + +template struct MpvProperty; + +template <> struct MpvProperty { + static void read(struct mpv_event_property *property) { + Q_ASSERT(property->format == MPV_FORMAT_NONE); + } +}; + +template <> struct MpvProperty { + static bool read(struct mpv_event_property *property) { + Q_ASSERT(property->format == MPV_FORMAT_FLAG); + if (!property->data) + qWarning("Property data data is null"); + return *static_cast(property->data) ? true : false; + } +}; + +template <> struct MpvProperty { + static qint64 read(struct mpv_event_property *property) { + Q_ASSERT(property->format == MPV_FORMAT_INT64); + if (!property->data) + qWarning("Property data data is null"); + return *static_cast(property->data); + } +}; + +template <> struct MpvProperty { + static double read(struct mpv_event_property *property) { + Q_ASSERT(property->format == MPV_FORMAT_DOUBLE); + if (!property->data) + qWarning("Property data data is null"); + return *static_cast(property->data); + } +}; + +template +decltype(auto) readProperty(struct mpv_event_property *property) { + return MpvProperty::read(property); +} + +void BackendMpv::processMpvEvents() { + struct mpv_event *event = nullptr; + do { + event = mpv_wait_event(m_handle, 0); + if (event->event_id == MPV_EVENT_NONE) + break; + qCDebug(mpvBackend).nospace() + << "Event " << mpv_event_name(event->event_id) << '(' << event->event_id + << "), error: " << event->error; + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: { + if (!event->data) + qCWarning(mpvBackend, "PROPERTY CHANGE data is null"); + auto property = static_cast(event->data); + qCDebug(mpvBackend) << "Property" << property->name << "changed"; + if (property->format == MPV_FORMAT_NONE) { + qCDebug(mpvBackend) << "No data in event"; + break; + } + if (strcmp(property->name, "pause") == 0) { + auto paused = readProperty(property); + auto state = paused ? PlayerPluginInterface::PlayState::Paused + : PlayerPluginInterface::PlayState::Playing; + if (!m_loadedFile) + state = PlayerPluginInterface::PlayState::Stopped; + m_player->playStateChanged(state); + } else if (strcmp(property->name, "duration") == 0) { + m_player->playbackDurationChanged( + static_cast( + readProperty(property))); + } else if (strcmp(property->name, "playback-time") == 0) { + m_player->playbackPositionChanged( + static_cast( + readProperty(property))); + } else if (strcmp(property->name, "ao-volume") == 0) { + if (m_volumeToSet > 0) { + qCDebug(mpvBackend) + << "Requested volume still not set, skipping this update"; + } else { + m_player->playbackVolumeChanged( + static_cast( + readProperty(property) / 100.0)); + } + } + } break; + case MPV_EVENT_LOG_MESSAGE: { + if (!event->data) + qCWarning(mpvBackend, "LOG MESSAGE data is null"); + auto log = static_cast(event->data); + QMessageLogger l{0, 0, 0, mpvLog().categoryName()}; + if (log->log_level <= MPV_LOG_LEVEL_ERROR) + l.critical() << log->text; + else if (log->log_level <= MPV_LOG_LEVEL_WARN) + l.warning() << log->text; + else if (log->log_level <= MPV_LOG_LEVEL_INFO) + l.info() << log->text; + else + l.debug() << log->text; + } break; + case MPV_EVENT_FILE_LOADED: { + int paused = 0; + mpv_get_property(m_handle, "paused", MPV_FORMAT_FLAG, &paused); + qCDebug(mpvBackend) << "file-loaded event!" << paused; + m_loadedFile = true; + auto state = paused ? PlayerPluginInterface::PlayState::Paused + : PlayerPluginInterface::PlayState::Playing; + m_player->playStateChanged(state); + } break; + case MPV_EVENT_END_FILE: { + m_loadedFile = false; + + if (!event->data) + qCWarning(mpvBackend, "END FILE data is null"); + auto endFile = static_cast(event->data); + if (endFile->reason == MPV_END_FILE_REASON_ERROR) + qCWarning(mpvBackend).nospace() + << "File ended due to error " << endFile->error << ": " + << mpv_error_string(endFile->error); + else + qCInfo(mpvBackend) << "File ended. Reason:" << endFile->reason; + m_player->playStateChanged(PlayerPluginInterface::PlayState::Stopped); + } break; + case MPV_EVENT_IDLE: { + m_player->playStateChanged(PlayerPluginInterface::PlayState::Stopped); + m_player->backendReadyToPlay(); + } break; + case MPV_EVENT_AUDIO_RECONFIG: { + if (m_volumeToSet >= 0) { + qCDebug(mpvBackend) << "Audio reconfigured, maybe it's ready now?"; + setVolume(m_volumeToSet); + } + } break; + default:; + } + } while (event->event_id != MPV_EVENT_NONE); +} + +void BackendMpv::mpvWakeupCb(void *obj) { + auto self = static_cast(obj); + QMetaObject::invokeMethod(self, "processMpvEvents", Qt::QueuedConnection); +} + +VideoRendererMpv::VideoRendererMpv(mpv_handle *handle, + VideoUpdateInterface *vui) + : m_handle{handle} { + qCDebug(mpvBackend, "VideoRendererMpv::VideoRendererMpv"); + m_oglCtx = static_cast( + mpv_get_sub_api(handle, MPV_SUB_API_OPENGL_CB)); + qCDebug(mpvBackend) << "created ogl ctx" << m_oglCtx; + if (!m_oglCtx) { + qCDebug(mpvBackend) << "Error obtaining mpv ogl context"; + return; + } + + qCDebug(mpvBackend, "setting callback"); + mpv_opengl_cb_set_update_callback(m_oglCtx, mpvUpdate, vui); + qCDebug(mpvBackend, "initializing gl context"); + int error = mpv_opengl_cb_init_gl(m_oglCtx, NULL, getProcAddress, this); + if (error) { + qCCritical(mpvBackend()) + << "Error initializing mpv ogl context:" << mpv_error_string(error); + m_oglCtx = nullptr; + } + qCDebug(mpvBackend, "all done"); +} + +VideoRendererMpv::~VideoRendererMpv() { + if (m_oglCtx) { + int error = mpv_opengl_cb_uninit_gl(m_oglCtx); + if (error) + qCWarning(mpvBackend) + << "Error uninitializing mpv ogl context:" << mpv_error_string(error); + } +} + +void VideoRendererMpv::render(QOpenGLFramebufferObject *fbo) { + int error = + mpv_opengl_cb_draw(m_oglCtx, + static_cast(fbo->handle()), // GLuint is unsigned + fbo->width(), fbo->height()); + if (error) + qCCritical(mpvBackend) << "Error rendering mpv frame:" + << mpv_error_string(error); +} + +#ifdef Q_OS_WIN +#include +#endif + +void *VideoRendererMpv::getProcAddress(void *, const char *name) { + const QByteArray ext{name}; + void *ret = reinterpret_cast( + QOpenGLContext::currentContext()->getProcAddress(ext)); +#ifdef Q_OS_WIN + if (!ret) { + HMODULE module = ::LoadLibraryA("opengl32.dll"); + ret = reinterpret_cast(::GetProcAddress(module, name)); + } +#endif + return ret; +} + +void VideoRendererMpv::mpvUpdate(void *obj) { + const auto vui = static_cast(obj); + vui->videoUpdated(); +} diff --git a/backendplugins/backend_mpv/backendmpv.h b/backendplugins/backend_mpv/backendmpv.h new file mode 100644 index 0000000..41b0d3a --- /dev/null +++ b/backendplugins/backend_mpv/backendmpv.h @@ -0,0 +1,61 @@ +#ifndef BACKENDMPV_H +#define BACKENDMPV_H + +#include +#include + +#include "backend_mpv_global.h" +#include "backendpluginbase.h" + +struct mpv_handle; +struct mpv_opengl_cb_context; + +class BACKEND_MPVSHARED_EXPORT BackendMpv : public QObject, + public BackendPluginBase { + Q_OBJECT + Q_PLUGIN_METADATA(IID ANIPLAYER_BACKEND_DPLUGIN_INTERFACE_IID FILE + "backend_mpv.json") + Q_INTERFACES(BackendPluginBase) + +public: + BackendMpv(); + virtual ~BackendMpv(); + bool initialize(PlayerPluginInterface *) override; + void deinitialize() override; + + VideoRendererBase *createRenderer(VideoUpdateInterface *) override; + + bool open(const QUrl &source) override; + void play() override; + void pause() override; + void stop() override; + + void seek(TimeStamp) override; + + void setVolume(Volume) override; + +private: + mpv_handle *m_handle = nullptr; + PlayerPluginInterface *m_player = nullptr; + Volume m_volumeToSet = -1; + bool m_loadedFile = false; + + Q_INVOKABLE void processMpvEvents(); + static void mpvWakeupCb(void *); +}; + +class BACKEND_MPVSHARED_EXPORT VideoRendererMpv : public VideoRendererBase { +public: + explicit VideoRendererMpv(mpv_handle *, VideoUpdateInterface *); + ~VideoRendererMpv() override; + void render(QOpenGLFramebufferObject *) override; + +private: + mpv_handle *m_handle = nullptr; + mpv_opengl_cb_context *m_oglCtx = nullptr; + + static void *getProcAddress(void *, const char *name); + static void mpvUpdate(void *); +}; + +#endif // BACKENDMPV_H diff --git a/backendplugins/backend_null/backend_null.json b/backendplugins/backend_null/backend_null.json new file mode 100644 index 0000000..e69de29 diff --git a/backendplugins/backend_null/backend_null.pro b/backendplugins/backend_null/backend_null.pro new file mode 100644 index 0000000..42eae60 --- /dev/null +++ b/backendplugins/backend_null/backend_null.pro @@ -0,0 +1,16 @@ +TARGET = backend_null +QT -= gui +TEMPLATE = lib +include(../../core/core.pri) + +DEFINES += BACKEND_NULL_LIBRARY QT_DEPRECATED_WARNINGS + +SOURCES += backendnull.cpp + +HEADERS += backendnull.h\ + backend_null_global.h + +unix { + target.path = /usr/lib + INSTALLS += target +} diff --git a/backendplugins/backend_null/backend_null_global.h b/backendplugins/backend_null/backend_null_global.h new file mode 100644 index 0000000..cdc30e3 --- /dev/null +++ b/backendplugins/backend_null/backend_null_global.h @@ -0,0 +1,12 @@ +#ifndef BACKEND_NULL_GLOBAL_H +#define BACKEND_NULL_GLOBAL_H + +#include + +#if defined(BACKEND_NULL_LIBRARY) +# define BACKEND_NULLSHARED_EXPORT Q_DECL_EXPORT +#else +# define BACKEND_NULLSHARED_EXPORT Q_DECL_IMPORT +#endif + +#endif // BACKEND_NULL_GLOBAL_H diff --git a/backendplugins/backend_null/backendnull.cpp b/backendplugins/backend_null/backendnull.cpp new file mode 100644 index 0000000..c09259e --- /dev/null +++ b/backendplugins/backend_null/backendnull.cpp @@ -0,0 +1,63 @@ +#include "backendnull.h" + +#include +#include + +BackendNull::BackendNull() +{ +} + +BackendNull::~BackendNull() +{ +} + +bool BackendNull::initialize(PlayerPluginInterface *) +{ + qDebug() << "Initialize"; + m_timer = new QTimer{this}; + m_timer->setInterval(1000); + return true; +} + +void BackendNull::deinitialize() +{ + qDebug() << "Deinitialize"; + delete m_timer; + m_timer = nullptr; +} + +bool BackendNull::open(const QUrl &source) +{ + qDebug() << "Opening " << source; + return true; +} + +void BackendNull::play() +{ + qDebug() << "Play"; +} + +void BackendNull::pause() +{ + qDebug() << "Pause"; +} + +void BackendNull::stop() +{ + qDebug() << "Stop"; +} + + +VideoRendererBase *BackendNull::createRenderer(VideoUpdateInterface *) +{ + return nullptr; +} + + +void BackendNull::seek(TimeStamp) +{ +} + +void BackendNull::setVolume(BackendPluginBase::Volume) +{ +} diff --git a/backendplugins/backend_null/backendnull.h b/backendplugins/backend_null/backendnull.h new file mode 100644 index 0000000..9915925 --- /dev/null +++ b/backendplugins/backend_null/backendnull.h @@ -0,0 +1,41 @@ +#ifndef BACKENDNULL_H +#define BACKENDNULL_H + +#include +#include + +#include "backend_null_global.h" +#include "backendpluginbase.h" + +class QTimer; + +class BACKEND_NULLSHARED_EXPORT BackendNull : public QObject, public BackendPluginBase +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID ANIPLAYER_BACKEND_DPLUGIN_INTERFACE_IID FILE "backend_null.json") + Q_INTERFACES(BackendPluginBase) + +public: + BackendNull(); + virtual ~BackendNull(); + bool initialize(PlayerPluginInterface *) override; + void deinitialize() override; + + bool open(const QUrl &source) override; + void play() override; + void pause() override; + void stop() override; + + + void seek(TimeStamp) override; + + void setVolume(Volume) override; + + VideoRendererBase *createRenderer(VideoUpdateInterface *) override; + +private: + QTimer *m_timer = nullptr; + +}; + +#endif // BACKENDNULL_H diff --git a/backendplugins/backendbuildconfig.pri b/backendplugins/backendbuildconfig.pri new file mode 100644 index 0000000..09a7862 --- /dev/null +++ b/backendplugins/backendbuildconfig.pri @@ -0,0 +1,2 @@ +include(../buildconfig.pri) +DESTDIR=../../build/backendplugins \ No newline at end of file diff --git a/backendplugins/backendplugins.pro b/backendplugins/backendplugins.pro new file mode 100644 index 0000000..5f3faf9 --- /dev/null +++ b/backendplugins/backendplugins.pro @@ -0,0 +1,15 @@ +TEMPLATE = subdirs + +include(../config.pri) + +!no_backend_null { + SUBDIRS += backend_null +} + +backend_mpv { + SUBDIRS += backend_mpv +} + +backend_qtav { + SUBDIRS += backend_qtav +} \ No newline at end of file diff --git a/buildconfig.pri b/buildconfig.pri new file mode 100644 index 0000000..7972793 --- /dev/null +++ b/buildconfig.pri @@ -0,0 +1,6 @@ +DESTDIR = ../build +CONFIG += c++14 +# Output Temporary files +OBJECTS_DIR = workfiles/obj +MOC_DIR = workfiles/moc +RCC_DIR = workfiles/rcc diff --git a/config.pri b/config.pri new file mode 100644 index 0000000..e69de29 diff --git a/core/aniplayer.exe.manifest b/core/aniplayer.exe.manifest new file mode 100644 index 0000000..4533801 --- /dev/null +++ b/core/aniplayer.exe.manifest @@ -0,0 +1,28 @@ + + + AniPlayer + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/aniplayer.rc b/core/aniplayer.rc new file mode 100644 index 0000000..bae35df --- /dev/null +++ b/core/aniplayer.rc @@ -0,0 +1,38 @@ +1 TYPELIB "aniplayer.rc" +IDI_ICON1 ICON DISCARDABLE "../resource/aniplayer-mikuru.ico" +1 24 "aniplayer.exe.manifest" +1 VERSIONINFO + FILEVERSION 3,0,0,0 + PRODUCTVERSION 3,0,0,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "aptx.org\0" + VALUE "FileDescription", "aniplayer\0" + VALUE "FileExtents", "xxx\0" + VALUE "FileOpenName", "Video Files (*.*)\0" + VALUE "FileVersion", "3, 0, 0, 0\0" + VALUE "InternalName", "aniplayer\0" + VALUE "LegalCopyright", "Copyright © 2017 APTX\0" + VALUE "MIMEType", "application/x-aniplayer\0" + VALUE "OriginalFilename", "aniplayer.exe\0" + VALUE "ProductName", "AniPlayer\0" + VALUE "ProductVersion", "3, 0, 0, 0\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END diff --git a/core/backendpluginbase.cpp b/core/backendpluginbase.cpp new file mode 100644 index 0000000..bdb08e1 --- /dev/null +++ b/core/backendpluginbase.cpp @@ -0,0 +1,2 @@ +#include "backendpluginbase.h" + diff --git a/core/backendpluginbase.h b/core/backendpluginbase.h new file mode 100644 index 0000000..58f1a68 --- /dev/null +++ b/core/backendpluginbase.h @@ -0,0 +1,38 @@ +#ifndef BACKENDPLUGINBASE_H +#define BACKENDPLUGINBASE_H + +#include "playerplugininterface.h" + +class QUrl; + +class BackendPluginBase { +public: + using TimeStamp = double; + // Volume valid range is 0.0-1.0 + using Volume = double; + + virtual ~BackendPluginBase() = default; + + virtual bool initialize(PlayerPluginInterface *) = 0; + virtual void deinitialize() = 0; + + virtual VideoRendererBase *createRenderer(VideoUpdateInterface *) = 0; + + virtual bool open(const QUrl &resource) = 0; + + virtual void play() = 0; + virtual void pause() = 0; + virtual void stop() = 0; + + virtual void seek(TimeStamp) = 0; + + virtual void setVolume(Volume) = 0; +}; + +#define ANIPLAYER_BACKEND_DPLUGIN_INTERFACE_IID \ + "org.aptx.aniplayer.BackendPluginInterface" + +#include +Q_DECLARE_INTERFACE(BackendPluginBase, ANIPLAYER_BACKEND_DPLUGIN_INTERFACE_IID) + +#endif // BACKENDPLUGINBASE_H diff --git a/core/core.pri b/core/core.pri new file mode 100644 index 0000000..1264d97 --- /dev/null +++ b/core/core.pri @@ -0,0 +1,2 @@ +INCLUDEPATH += $$PWD +include(../buildconfig.pri) diff --git a/core/core.pro b/core/core.pro new file mode 100644 index 0000000..5e7a100 --- /dev/null +++ b/core/core.pro @@ -0,0 +1,36 @@ +TEMPLATE = app + +QT += widgets qml quick +CONFIG += c++11 + +TARGET = aniplayer + +include(core.pri) + +SOURCES += main.cpp \ + player.cpp \ + pluginmanager.cpp \ + videoelement.cpp \ + timeformatter.cpp \ + instancemanager.cpp + +RESOURCES += qml.qrc + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +HEADERS += \ + player.h \ + backendpluginbase.h \ + pluginmanager.h \ + videoelement.h \ + playerplugininterface.h \ + timeformatter.h \ + instancemanager.h + +include(qtsingleapplication/qtsingleapplication.pri) + +win32 { + CONFIG -= embed_manifest_exe + RC_FILE += aniplayer.rc +} diff --git a/core/instancemanager.cpp b/core/instancemanager.cpp new file mode 100644 index 0000000..74b2454 --- /dev/null +++ b/core/instancemanager.cpp @@ -0,0 +1,64 @@ +#include "instancemanager.h" + +#include +#include +#include + +#include "timeformatter.h" + +Q_LOGGING_CATEGORY(imCategory, "InstanceManager") + +InstanceManager::InstanceManager(QObject *parent) : QObject(parent) { + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("files", "Files to play", "[files...]"); + parser.addOption(backendOption); + parser.addOption(uiOption); + parser.addOption(positionOption); +} + +int InstanceManager::runInstance(const QCoreApplication &app) { + parser.process(app); + + const auto positionalArgs = parser.positionalArguments(); + + QQmlApplicationEngine engine; + + if (!positionalArgs.empty()) + player.setNextSource(QUrl::fromUserInput(positionalArgs[0])); + qCDebug(imCategory, "Player Created"); + TimeFormatter timeFormatter; + engine.rootContext()->setContextProperty("player", &player); + engine.rootContext()->setContextProperty("timeFormatter", &timeFormatter); + qCDebug(imCategory, "Player Added"); + engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + qCDebug(imCategory, "QML engine loaded"); + return app.exec(); +} + +void InstanceManager::handleSingleInstanceMessage(const QString &message) { + const QByteArray base64 = message.toLatin1(); + const QByteArray arr = QByteArray::fromBase64(base64); + QDataStream stream(arr); + QStringList args; + stream >> args; + + if (stream.status() != QDataStream::Ok) { + qCWarning(imCategory) + << "Failed to read serialized single instance message"; + return; + } + if (!parser.parse(args)) { + qCWarning(imCategory) + << "Failed to parse arguments from single instance message"; + return; + } + + const auto positionalArgs = parser.positionalArguments(); + if (positionalArgs.empty()) { + qCInfo(imCategory()) << "No new file to open"; + return; + } + + player.loadAndPlay(QUrl::fromUserInput(positionalArgs[0])); +} diff --git a/core/instancemanager.h b/core/instancemanager.h new file mode 100644 index 0000000..e337139 --- /dev/null +++ b/core/instancemanager.h @@ -0,0 +1,32 @@ +#ifndef INSTANCEMANAGER_H +#define INSTANCEMANAGER_H + +#include + +#include +#include +#include + +#include "player.h" + +class InstanceManager : public QObject { + Q_OBJECT +public: + explicit InstanceManager(QObject *parent = 0); + + int runInstance(const QCoreApplication &app); +public slots: + void handleSingleInstanceMessage(const QString &message); + +private: + const QCommandLineOption backendOption{"backend", "Use backend", + "name of backend plugin", "default"}; + const QCommandLineOption uiOption{"ui", "Use ui", "name of ui", "default"}; + const QCommandLineOption positionOption{"position", + "Start playback specific position", + "position in seconds", "0"}; + QCommandLineParser parser; + Player player; +}; + +#endif // INSTANCEMANAGER_H diff --git a/core/main.cpp b/core/main.cpp new file mode 100644 index 0000000..0ee70f9 --- /dev/null +++ b/core/main.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include +#include + +#include "instancemanager.h" +#include "player.h" +#include "videoelement.h" + +#include + +int main(int argc, char *argv[]) { + QtSingleApplication app(argc, argv); + app.setApplicationDisplayName("AniPlayer"); + app.setApplicationName("AniPlayer"); + app.setApplicationVersion("1.0"); + + InstanceManager im; + QObject::connect(&app, SIGNAL(messageReceived(QString)), &im, + SLOT(handleSingleInstanceMessage(QString))); + + if (app.isRunning()) { + QByteArray arr; + { + QDataStream stream{&arr, QIODevice::WriteOnly}; + stream << app.arguments(); + } + const auto base64 = arr.toBase64(); + if (app.sendMessage(QString::fromUtf8(base64))) + return 0; + return 1; + } + + qmlRegisterType("org.aptx.aniplayer", 1, 0, "VideoElement"); + Player::reqisterQmlTypes(); + + try { + return im.runInstance(app); + } catch (const std::exception &ex) { + qDebug("Exception: %s", ex.what()); + } + return 1; +} diff --git a/core/player.cpp b/core/player.cpp new file mode 100644 index 0000000..a1e4098 --- /dev/null +++ b/core/player.cpp @@ -0,0 +1,218 @@ +#include "player.h" + +#include +#include + +Q_LOGGING_CATEGORY(playerCategory, "Player") + +Player::Player(QObject *parent) : QObject(parent) { loadBackend(); } + +BackendPluginBase *Player::backend() const { return m_backend; } + +bool Player::hasRenderer() const { return m_renderer != nullptr; } + +QUrl Player::currentSource() const { return m_currentSource; } + +QUrl Player::nextSource() const { return m_nextSource; } + +Player::PlayState Player::state() const { return m_state; } + +Player::Volume Player::volume() const { return m_volume; } + +bool Player::muted() const { return m_muted; } + +Player::AudioStreams Player::availableAudioStreams() const { + return m_availableAudioStreams; +} + +Player::StreamIndex Player::currentAudioStream() const { + return m_currentAudioStream; +} + +Player::StreamIndex Player::currentVideoStream() const { + return m_currentVideoStream; +} + +Player::StreamIndex Player::currentSubtitleStream() const { + return m_currentSubtitleStream; +} + +Player::VideoStreams Player::availableVideoStreams() const { + return m_availableVideoStreams; +} + +Player::SubtitleStreams Player::availableSubtitleStreams() const { + return m_availableSubtitleStreams; +} + +Player::TimeStamp Player::duration() const { return m_duration; } + +PlayerPluginInterface::TimeStamp Player::position() const { return m_position; } + +void Player::load(const QUrl &resource) { + if (canLoadVideoNow()) + m_backend->open(resource); + else + setNextSource(resource); +} + +void Player::loadAndPlay(const QUrl &resource) { + load(resource); + play(); +} + +void Player::setNextSource(QUrl nextSource) { + if (m_nextSource == nextSource) + return; + + m_nextSource = nextSource; + emit nextSourceChanged(nextSource); +} + +void Player::play() { m_backend->play(); } + +void Player::pause() { m_backend->pause(); } + +void Player::stop() { m_backend->stop(); } + +void Player::togglePlay() { PlayState::Playing == state() ? pause() : play(); } + +void Player::seek(Player::TimeStamp position) { m_backend->seek(position); } + +void Player::setVolume(Volume volume) { + volume = qBound(Volume{}, volume, MAX_VOLUME); + m_backend->setVolume(volume); +} + +void Player::volumeUp(int byPercentagePoints) { + Volume realValue = + static_cast(byPercentagePoints) / Volume{100.0} * MAX_VOLUME; + setVolume(volume() + realValue); +} + +void Player::volumeDown(int byPercentagePoints) { + volumeUp(-byPercentagePoints); +} + +void Player::setMuted(bool muted) { + if (m_muted == muted) + return; + + m_muted = muted; + emit mutedChanged(muted); +} + +void Player::toggleMuted() { setMuted(!muted()); } + +void Player::setCurrentAudioStream(Player::StreamIndex currentAudioStream) { + if (m_currentAudioStream == currentAudioStream) + return; + + m_currentAudioStream = currentAudioStream; + emit currentAudioStreamChanged(currentAudioStream); +} + +void Player::setCurrentVideoStream(Player::StreamIndex currentVideoStream) { + if (m_currentVideoStream == currentVideoStream) + return; + + m_currentVideoStream = currentVideoStream; + emit currentVideoStreamChanged(currentVideoStream); +} + +void Player::setCurrentSubtitleStream( + Player::StreamIndex currentSubtitleStream) { + if (m_currentSubtitleStream == currentSubtitleStream) + return; + + m_currentSubtitleStream = currentSubtitleStream; + emit currentSubtitleStreamChanged(currentSubtitleStream); +} + +void Player::backendReadyToPlay() { + m_backendReady = true; + if (canLoadVideoNow()) + loadNextFile(); +} + +void Player::rendererSinkSet(VideoUpdateInterface *renderer) { + m_renderer = renderer; + m_rendererReady = false; +} + +void Player::rendererReady() { + m_rendererReady = true; + if (canLoadVideoNow()) + loadNextFile(); +} + +void Player::playStateChanged(PlayerPluginInterface::PlayState state) { + auto s = static_cast(state); + // if (currentSource().isEmpty()) s = PlayState::Stopped; + if (m_state == s) + return; + m_state = s; + qCDebug(playerCategory) << "Play state changed to" << s; + emit stateChanged(s); +} + +void Player::playbackDurationChanged( + PlayerPluginInterface::TimeStamp duration) { + if (qFuzzyCompare(m_duration, duration)) + return; + + qCDebug(playerCategory) << "Duration changed to" << duration; + m_duration = duration; + emit durationChanged(duration); +} + +void Player::playbackPositionChanged( + PlayerPluginInterface::TimeStamp position) { + if (qFuzzyCompare(m_position, position)) + return; + + qCDebug(playerCategory) << "Duration changed to" << position; + m_position = position; + emit positionChanged(position); +} + +void Player::playbackVolumeChanged(Player::Volume volume) { + qCDebug(playerCategory) << "Volume changed to" << volume; + if (qFuzzyCompare(m_volume, volume)) + return; + + qCDebug(playerCategory) << "Volume changed to!!" << volume; + m_volume = volume; + emit volumeChanged(volume); +} + +void Player::streamsChanged() {} + +void Player::reqisterQmlTypes() { + qRegisterMetaType("TimeStamp"); + qRegisterMetaType("StreamIndex"); + qRegisterMetaType("Volume"); + qmlRegisterType("org.aptx.aniplayer", 1, 0, "Player"); +} + +void Player::loadBackend() { + m_pluginManager.setPluginDirectory("backendplugins"); + m_pluginManager.setPluginPrefix("backend"); + m_pluginManager.loadDefaultPlugin(); + m_backend = m_pluginManager.instance(); + if (!m_backend) + throw std::runtime_error{std::string("Failed to load backend: ") + + qPrintable(m_pluginManager.errorString())}; + m_backend->initialize(this); + qCDebug(playerCategory) << "Loaded backend" << m_backend; +} + +bool Player::canLoadVideoNow() const { + return m_backendReady && m_renderer && m_rendererReady; +} + +void Player::loadNextFile() { + if (!m_nextSource.isEmpty()) + loadAndPlay(m_nextSource); + setNextSource(QUrl{}); +} diff --git a/core/player.h b/core/player.h new file mode 100644 index 0000000..51cc43e --- /dev/null +++ b/core/player.h @@ -0,0 +1,176 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include +#include +#include +#include + +#include "backendpluginbase.h" +#include "pluginmanager.h" + +class Player : public QObject, + public PlayerPluginInterface, + public PlayerRendererInterface { + Q_OBJECT + Q_PROPERTY(QUrl currentSource READ currentSource WRITE load NOTIFY + currentSourceChanged) + Q_PROPERTY(QUrl nextSource READ nextSource WRITE setNextSource NOTIFY + nextSourceChanged) + + Q_PROPERTY(Player::TimeStamp duration READ duration NOTIFY durationChanged) + Q_PROPERTY(Player::TimeStamp position READ position WRITE seek NOTIFY + positionChanged) + + Q_PROPERTY(Player::PlayState state READ state NOTIFY stateChanged) + Q_PROPERTY(double volume READ volume WRITE setVolume NOTIFY volumeChanged) + Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged) + + Q_PROPERTY(Player::StreamIndex currentAudioStream READ currentAudioStream + WRITE setCurrentAudioStream NOTIFY currentAudioStreamChanged) + Q_PROPERTY(Player::StreamIndex currentVideoStream READ currentVideoStream + WRITE setCurrentVideoStream NOTIFY currentVideoStreamChanged) + Q_PROPERTY( + Player::StreamIndex currentSubtitleStream READ currentSubtitleStream WRITE + setCurrentSubtitleStream NOTIFY currentSubtitleStreamChanged) + + Q_PROPERTY(Player::AudioStreams availableAudioStreams READ + availableAudioStreams NOTIFY availableAudioStreamsChanged) + Q_PROPERTY(Player::VideoStreams availableVideoStreams READ + availableVideoStreams NOTIFY availableVideoStreamsChanged) + Q_PROPERTY( + Player::SubtitleStreams availableSubtitleStreams READ + availableSubtitleStreams NOTIFY availableSubtitleStreamsChanged) + +public: + using StreamIndex = int; + using AudioStreams = QList; + using VideoStreams = QList; + using SubtitleStreams = QList; + using Volume = double; + using TimeStamp = PlayerPluginInterface::TimeStamp; + + static const constexpr Volume MAX_VOLUME = Volume{1.0}; + + explicit Player(QObject *parent = 0); + // ~Player() /*override*/; + + enum class PlayState { + Stopped = static_cast(PlayerPluginInterface::PlayState::Stopped), + Paused = static_cast(PlayerPluginInterface::PlayState::Paused), + Playing = static_cast(PlayerPluginInterface::PlayState::Playing), + }; + Q_ENUM(PlayState) + + BackendPluginBase *backend() const; + bool hasRenderer() const; + + QUrl currentSource() const; + QUrl nextSource() const; + + PlayState state() const; + Volume volume() const; + bool muted() const; + + StreamIndex currentAudioStream() const; + StreamIndex currentVideoStream() const; + StreamIndex currentSubtitleStream() const; + + AudioStreams availableAudioStreams() const; + VideoStreams availableVideoStreams() const; + SubtitleStreams availableSubtitleStreams() const; + + Player::TimeStamp duration() const; + Player::TimeStamp position() const; + +signals: + void stateChanged(PlayState state); + void volumeChanged(Volume volume); + void mutedChanged(bool muted); + + void availableAudioStreamsChanged(AudioStreams availableAudioStreams); + void availableVideoStreamsChanged(VideoStreams availableVideoStreams); + void + availableSubtitleStreamsChanged(SubtitleStreams availableSubtitleStreams); + + void currentAudioStreamChanged(int currentAudioStream); + void currentVideoStreamChanged(StreamIndex currentVideoStream); + void currentSubtitleStreamChanged(StreamIndex currentSubtitleStream); + + void currentSourceChanged(QUrl currentSource); + void nextSourceChanged(QUrl nextSource); + + void durationChanged(Player::TimeStamp duration); + void positionChanged(Player::TimeStamp position); + +public slots: + // Basic Play state + void load(const QUrl &resource); + void loadAndPlay(const QUrl &resource); + void setNextSource(QUrl nextSource); + + void play(); + void pause(); + void stop(); + + void togglePlay(); + + void seek(Player::TimeStamp position); + + // Volume + void setVolume(Volume volume); + void volumeUp(int byPercentagePoints = 5); + void volumeDown(int byPercentagePoints = 5); + + void setMuted(bool muted); + void toggleMuted(); + + // Streams + void setCurrentAudioStream(StreamIndex currentAudioStream); + void setCurrentVideoStream(StreamIndex currentVideoStream); + void setCurrentSubtitleStream(StreamIndex currentSubtitleStream); + +protected: + void backendReadyToPlay() override; + void rendererSinkSet(VideoUpdateInterface *) override; + void rendererReady() override; + void playStateChanged(PlayerPluginInterface::PlayState) override; + void playbackDurationChanged(TimeStamp) override; + void playbackPositionChanged(TimeStamp) override; + void playbackVolumeChanged(Volume) override; + void streamsChanged() override; + +public: + static void reqisterQmlTypes(); + +private: + void loadBackend(); + bool canLoadVideoNow() const; + void loadNextFile(); + + PluginManager m_pluginManager; + BackendPluginBase *m_backend = nullptr; + QUrl m_currentSource; + PlayState m_state = PlayState::Stopped; + Volume m_volume = MAX_VOLUME; + AudioStreams m_availableAudioStreams; + StreamIndex m_currentAudioStream = StreamIndex{}; + StreamIndex m_currentVideoStream = StreamIndex{}; + StreamIndex m_currentSubtitleStream = StreamIndex{}; + VideoStreams m_availableVideoStreams; + SubtitleStreams m_availableSubtitleStreams; + Player::TimeStamp m_duration = 0; + Player::TimeStamp m_position = 0; + QUrl m_nextSource; + VideoUpdateInterface *m_renderer = nullptr; + bool m_muted = false; + bool m_backendReady = false; + bool m_rendererReady = false; +}; + +Q_DECLARE_METATYPE(Player::PlayState) +Q_DECLARE_METATYPE(Player::StreamIndex) +Q_DECLARE_METATYPE(Player::TimeStamp) +// Q_DECLARE_METATYPE(Player::Volume) + +#endif // PLAYER_H diff --git a/core/playerplugininterface.cpp b/core/playerplugininterface.cpp new file mode 100644 index 0000000..106c287 --- /dev/null +++ b/core/playerplugininterface.cpp @@ -0,0 +1 @@ +#include "playerplugininterface.h" diff --git a/core/playerplugininterface.h b/core/playerplugininterface.h new file mode 100644 index 0000000..a4b62c9 --- /dev/null +++ b/core/playerplugininterface.h @@ -0,0 +1,64 @@ +#ifndef PLAYERPLUGININTERFACE_H +#define PLAYERPLUGININTERFACE_H + +#include + +class QOpenGLFramebufferObject; + +class VideoUpdateInterface { +public: + virtual ~VideoUpdateInterface() = default; + + virtual void videoUpdated() = 0; +}; + +class PlayerPluginInterface { +public: + using TimeStamp = double; + using Volume = double; + + enum class PlayState { Stopped, Paused, Playing }; + /* + * .-----. + * | | Error + * v | + * Stopped -'<--+<-------. + * | ^ | + * | Load | Error | + * v | | + * Paused<------+ | File End + * | ^ | + * | Play | Pause | + * v | | + * Playing------+--------' + */ + + virtual ~PlayerPluginInterface() = default; + + virtual void backendReadyToPlay() = 0; + + virtual void playStateChanged(PlayState) = 0; + virtual void playbackDurationChanged(TimeStamp) = 0; + virtual void playbackPositionChanged(TimeStamp) = 0; + virtual void playbackVolumeChanged(Volume) = 0; + + virtual void streamsChanged() = 0; +}; + +class PlayerRendererInterface { +public: + virtual ~PlayerRendererInterface() = default; + virtual void rendererSinkSet(VideoUpdateInterface *) = 0; + virtual void rendererReady() = 0; +}; + +class VideoRendererBase { +public: + VideoRendererBase() = default; + VideoRendererBase(const VideoRendererBase &) = delete; + VideoRendererBase &operator=(const VideoRendererBase &) = delete; + virtual ~VideoRendererBase() = default; + virtual void render(QOpenGLFramebufferObject *) = 0; +}; + +#endif // PLAYERPLUGININTERFACE_H diff --git a/core/pluginmanager.cpp b/core/pluginmanager.cpp new file mode 100644 index 0000000..cec097a --- /dev/null +++ b/core/pluginmanager.cpp @@ -0,0 +1,50 @@ +#include "pluginmanager.h" + +PluginManager::PluginManager() {} + +QString PluginManager::errorString() const { return m_loader.errorString(); } + +QString PluginManager::pluginDirectory() const { return m_pluginDirectory; } + +QString PluginManager::pluginPrefix() const { return m_pluginPrefix; } + +void PluginManager::setPluginDirectory(QString pluginDirectory) { + if (m_pluginDirectory == pluginDirectory) + return; + + m_pluginDirectory = pluginDirectory; + emit pluginDirectoryChanged(pluginDirectory); +} + +void PluginManager::setPluginPrefix(QString pluginPrefix) { + if (m_pluginPrefix == pluginPrefix) + return; + + m_pluginPrefix = pluginPrefix; + emit pluginPrefixChanged(pluginPrefix); +} + +bool PluginManager::load(const QString &plugin) { + if (m_loader.isLoaded()) + if (!m_loader.unload()) + return false; + + QString pluginPath = QString{"%1/%2_%3"} + .arg(m_pluginDirectory) + .arg(m_pluginPrefix) + .arg(plugin); + m_loader.setFileName(pluginPath); + + m_loader.load(); + + return m_loader.isLoaded(); +} + +QObject *PluginManager::qObjectInstance() { + if (!m_loader.isLoaded()) + return nullptr; + + return m_loader.instance(); +} + +void PluginManager::loadDefaultPlugin() { load("mpv"); } diff --git a/core/pluginmanager.h b/core/pluginmanager.h new file mode 100644 index 0000000..245e7d5 --- /dev/null +++ b/core/pluginmanager.h @@ -0,0 +1,44 @@ +#ifndef PLUGINMANAGER_H +#define PLUGINMANAGER_H + +#include +#include + +class PluginManager : public QObject { + Q_OBJECT + Q_PROPERTY(QString pluginDirectory READ pluginDirectory WRITE + setPluginDirectory NOTIFY pluginDirectoryChanged) + Q_PROPERTY(QString pluginPrefix READ pluginPrefix WRITE setPluginPrefix NOTIFY + pluginPrefixChanged) +public: + PluginManager(); + + QString errorString() const; + + QString pluginDirectory() const; + QString pluginPrefix() const; + + template Interface *instance() { + return qobject_cast(qObjectInstance()); + } + +public slots: + void setPluginDirectory(QString pluginDirectory); + void setPluginPrefix(QString pluginPrefix); + + bool load(const QString &plugin); + QObject *qObjectInstance(); + + void loadDefaultPlugin(); + +signals: + void pluginDirectoryChanged(QString pluginDirectory); + void pluginPrefixChanged(QString pluginPrefix); + +private: + QString m_pluginDirectory; + QString m_pluginPrefix; + QPluginLoader m_loader; +}; + +#endif // PLUGINMANAGER_H diff --git a/core/qml.qrc b/core/qml.qrc new file mode 100644 index 0000000..0e27bae --- /dev/null +++ b/core/qml.qrc @@ -0,0 +1,10 @@ + + + qml/main.qml + qml/OpenButton.qml + qml/PlayerControls.qml + qml/SeekSlider.qml + qml/PlaybackPosition.qml + qml/VolumeSlider.qml + + diff --git a/core/qml/Button.qml b/core/qml/Button.qml new file mode 100644 index 0000000..1cb984d --- /dev/null +++ b/core/qml/Button.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 + +Item { + id: buttonRoot + property alias text: label.text + + signal clicked + + width: 80 + height: label.lineHeight + + Text { + id: label + color: "red" + font.pointSize: 24 + anchors.fill: parent + } + MouseArea { + id: ma + anchors.fill: parent + onClicked: buttonRoot.clicked() + } +} diff --git a/core/qml/OpenButton.qml b/core/qml/OpenButton.qml new file mode 100644 index 0000000..4e2f967 --- /dev/null +++ b/core/qml/OpenButton.qml @@ -0,0 +1,24 @@ +import QtQuick 2.0 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 1.4 + +Button { + signal openFileRequested(string file) + + text: "Open" + + FileDialog { + id: fileDialog + title: "Play file" + onAccepted: { + openFileRequested(fileUrl) + } + } + onClicked: { + open(); + } + + function open() { + fileDialog.open(); + } +} diff --git a/core/qml/PlaybackPosition.qml b/core/qml/PlaybackPosition.qml new file mode 100644 index 0000000..c9db341 --- /dev/null +++ b/core/qml/PlaybackPosition.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 +import QtQml 2.2 + +Item { + property double duration: 0 + property double position: 0 +Text { + text: { + if (!timeFormatter) return ""; + var durationStr = timeFormatter.format(duration); + var positionStr = timeFormatter.format(position); + return positionStr + " / " + durationStr; + } + color: "red" + anchors.centerIn: parent +} +} diff --git a/core/qml/PlayerControls.qml b/core/qml/PlayerControls.qml new file mode 100644 index 0000000..ceabcf8 --- /dev/null +++ b/core/qml/PlayerControls.qml @@ -0,0 +1,111 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.4 +import QtQuick.Window 2.2 +import org.aptx.aniplayer 1.0 + +Row { + property Player controlledPlayer: null + property Window controlledWindow: null + + enabled: controlledPlayer !== null && controlledWindow !== null + + function toggleFullScreen() { + fullscreenButton.checked = !fullscreenButton.checked; + } + + OpenButton { + id: openButton + text: "Open" + onOpenFileRequested: { + controlledPlayer.loadAndPlay(file) + } + } + Button { + id: togglePlayButton + text: "Play" + onClicked: { + console.log("STATE") + console.log(controlledPlayer.state) + console.log(Player.Stopped) + if (controlledPlayer.state === Player.Stopped) { + openButton.open() + } else { + controlledPlayer.togglePlay() + } + } + state: { + if (!controlledPlayer) return ""; + return controlledPlayer.state === Player.Playing ? "pause" : "" + } + states: [ + State { + name: "pause" + PropertyChanges { + target: togglePlayButton + text: "Pause" + } + } + ] + } + Button { + id: fullscreenButton + text: "FS" + checkable: true + onCheckedChanged: { + if (!checked) { + console.log("show normal") + controlledWindow.showNormal(); + } else { + console.log("show fs") + controlledWindow.showFullScreen(); + } + } + } + Button { + id: stayOnTopButton + text: "OnTop" + enabled: !controlledWindow.isFullScreen() + checkable: true + onCheckedChanged: { + if (!checked) { + controlledWindow.flags = controlledWindow.flags & ~Qt.WindowStaysOnTopHint + } else { + controlledWindow.flags = controlledWindow.flags | Qt.WindowStaysOnTopHint + } + } + } + Button { + id: framelessButton + text: "Frameless" + enabled: !controlledWindow.isFullScreen() + checkable: true + onCheckedChanged: { + if (!checked) { + controlledWindow.flags = controlledWindow.flags & ~Qt.FramelessWindowHint + } else { + controlledWindow.flags = controlledWindow.flags | Qt.FramelessWindowHint + } + } + } + SeekSlider { + width: 200 + height: fullscreenButton.height + id: ss + duration: controlledPlayer ? controlledPlayer.duration : 0 + position: controlledPlayer ? controlledPlayer.position : 0 + onSeekRequested: controlledPlayer.seek(position) + } + PlaybackPosition { + height: fullscreenButton.height + width: 200 + duration: controlledPlayer ? controlledPlayer.duration : 0 + position: controlledPlayer ? controlledPlayer.position : 0 + } + VolumeSlider { + height: fullscreenButton.height + width: 100 + volume: controlledPlayer ? controlledPlayer.volume : 1 + onVolumeChangeRequested: controlledPlayer.setVolume(volume) + } +} + diff --git a/core/qml/SeekSlider.qml b/core/qml/SeekSlider.qml new file mode 100644 index 0000000..e6a392e --- /dev/null +++ b/core/qml/SeekSlider.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import org.aptx.aniplayer 1.0 + +Item { + property double duration: 0 + property double position: 0 + + signal seekRequested(double position) + + enabled: duration > 1 + + Rectangle { + id: watched + color: "#00BB00" + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: { + return duration ? position / duration * parent.width : 0; + } + } + + Rectangle { + id: unwatched + color: "#FF0000" + anchors.left: watched.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + MouseArea { + id: ma + anchors.fill: parent + onClicked: { + var pos = mouseX / parent.width * duration; + console.log("seek clicked " + pos) + seekRequested(pos); + } + } +} diff --git a/core/qml/VolumeSlider.qml b/core/qml/VolumeSlider.qml new file mode 100644 index 0000000..5e68d5f --- /dev/null +++ b/core/qml/VolumeSlider.qml @@ -0,0 +1,37 @@ +import QtQuick 2.0 + +Item { + signal volumeChangeRequested(double volume) + + property double maxVolume: 1.0 + property double volume: 0.0 + + Rectangle { + id: heard + color: "#00B000" + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: { + return maxVolume ? volume / maxVolume * parent.width : 0; + } + } + + Rectangle { + id: unheard + color: "#F00000" + anchors.left: heard.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + MouseArea { + id: ma + anchors.fill: parent + onClicked: { + var vol = mouseX / parent.width * maxVolume; + volumeChangeRequested(vol); + } + } +} diff --git a/core/qml/main.qml b/core/qml/main.qml new file mode 100644 index 0000000..b52ec00 --- /dev/null +++ b/core/qml/main.qml @@ -0,0 +1,96 @@ +import QtQuick.Window 2.2 +import org.aptx.aniplayer 1.0 +import QtQuick 2.7 + +Window { + id: window + visible: true + width: 300 + height: 300 + property int clicks: 0 + //property Visibility previousVisibility: Window.Normal + + function isFullScreen() { + return visibility === Window.FullScreen + } + + VideoElement { + source: player + anchors.fill: parent + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + property int origX + property int origY + property bool isBeingDragged: false + property bool ignoreOnClick: false + + onWheel: { + if (wheel.angleDelta.y > 0) + player.volumeUp(5); + else + player.volumeDown(5); + } + + onPressed: { + console.log("isBeingDragged", isBeingDragged); + if (isBeingDragged) + return; + console.log("setting orig"); + origX = mouseX + origY = mouseY + } + + onPositionChanged: { + if (window.isFullScreen()) return; + if (!(mouse.buttons & Qt.LeftButton)) + return; + window.x += mouseX - origX + window.y += mouseY - origY + isBeingDragged = true; + } + + onReleased: { + console.log("mouse.buttons", mouse.buttons, Qt.NoButton); + if ((mouse.buttons != Qt.NoButton)) return; + if (isBeingDragged) { + isBeingDragged = false; + ignoreOnClick = true; + } + } + + onClicked: { + if (isBeingDragged) { + return; + } + if (ignoreOnClick) { + ignoreOnClick = false; + return; + } + console.log(mouse.button); + if (mouse.button === Qt.LeftButton) + controls.toggleVisible(); + else if (mouse.button === Qt.RightButton) + controls.toggleFullScreen(); + else if (mouse.button === Qt.MiddleButton) + player.togglePlay(); + } + cursorShape: !controls.visible && window.visibility === Window.FullScreen ? Qt.BlankCursor : Qt.ArrowCursor; + } + + PlayerControls { + id: controls + controlledPlayer: player + controlledWindow: window + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + function toggleVisible() { + visible = !visible; + } + } +} diff --git a/core/qtsingleapplication/QtLockedFile b/core/qtsingleapplication/QtLockedFile new file mode 100644 index 0000000..16b48ba --- /dev/null +++ b/core/qtsingleapplication/QtLockedFile @@ -0,0 +1 @@ +#include "qtlockedfile.h" diff --git a/core/qtsingleapplication/QtSingleApplication b/core/qtsingleapplication/QtSingleApplication new file mode 100644 index 0000000..d111bf7 --- /dev/null +++ b/core/qtsingleapplication/QtSingleApplication @@ -0,0 +1 @@ +#include "qtsingleapplication.h" diff --git a/core/qtsingleapplication/qtlocalpeer.cpp b/core/qtsingleapplication/qtlocalpeer.cpp new file mode 100644 index 0000000..e4cf804 --- /dev/null +++ b/core/qtsingleapplication/qtlocalpeer.cpp @@ -0,0 +1,203 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + + +#include "qtlocalpeer.h" +#include +#include +#include + +#if defined(Q_OS_WIN) +#include +#include +typedef BOOL(WINAPI*PProcessIdToSessionId)(DWORD,DWORD*); +static PProcessIdToSessionId pProcessIdToSessionId = 0; +#endif +#if defined(Q_OS_UNIX) +#include +#endif + +namespace QtLP_Private { +#include "qtlockedfile.cpp" +#if defined(Q_OS_WIN) +#include "qtlockedfile_win.cpp" +#else +#include "qtlockedfile_unix.cpp" +#endif +} + +const char* QtLocalPeer::ack = "ack"; + +QtLocalPeer::QtLocalPeer(QObject* parent, const QString &appId) + : QObject(parent), id(appId) +{ + QString prefix = id; + if (id.isEmpty()) { + id = QCoreApplication::applicationFilePath(); +#if defined(Q_OS_WIN) + id = id.toLower(); +#endif + prefix = id.section(QLatin1Char('/'), -1); + } + prefix.remove(QRegExp("[^a-zA-Z]")); + prefix.truncate(6); + + QByteArray idc = id.toUtf8(); + quint16 idNum = qChecksum(idc.constData(), idc.size()); + socketName = QLatin1String("qtsingleapp-") + prefix + + QLatin1Char('-') + QString::number(idNum, 16); + +#if defined(Q_OS_WIN) + if (!pProcessIdToSessionId) { + QLibrary lib("kernel32"); + pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId"); + } + if (pProcessIdToSessionId) { + DWORD sessionId = 0; + pProcessIdToSessionId(GetCurrentProcessId(), &sessionId); + socketName += QLatin1Char('-') + QString::number(sessionId, 16); + } +#else + { + using namespace QtLP_Private; + socketName += QLatin1Char('-') + QString::number(getuid(), 16); + } +#endif + + server = new QLocalServer(this); + QString lockName = QDir(QDir::tempPath()).absolutePath() + + QLatin1Char('/') + socketName + + QLatin1String("-lockfile"); + lockFile.setFileName(lockName); + lockFile.open(QIODevice::ReadWrite); +} + + + +bool QtLocalPeer::isClient() +{ + if (lockFile.isLocked()) + return false; + + if (!lockFile.lock(QtLP_Private::QtLockedFile::WriteLock, false)) + return true; + + bool res = server->listen(socketName); +#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(4,5,0)) + // ### Workaround + if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { + QFile::remove(QDir::cleanPath(QDir::tempPath())+QLatin1Char('/')+socketName); + res = server->listen(socketName); + } +#endif + if (!res) + qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); + QObject::connect(server, SIGNAL(newConnection()), SLOT(receiveConnection())); + return false; +} + + +bool QtLocalPeer::sendMessage(const QString &message, int timeout) +{ + if (!isClient()) + return false; + + QLocalSocket socket; + bool connOk = false; + for(int i = 0; i < 2; i++) { + // Try twice, in case the other instance is just starting up + socket.connectToServer(socketName); + connOk = socket.waitForConnected(timeout/2); + if (connOk || i) + break; + int ms = 250; +#if defined(Q_OS_WIN) + Sleep(DWORD(ms)); +#else + struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 }; + nanosleep(&ts, NULL); +#endif + } + if (!connOk) + return false; + + QByteArray uMsg(message.toUtf8()); + QDataStream ds(&socket); + ds.writeBytes(uMsg.constData(), uMsg.size()); + bool res = socket.waitForBytesWritten(timeout); + if (res) { + res &= socket.waitForReadyRead(timeout); // wait for ack + if (res) + res &= (socket.read(qstrlen(ack)) == ack); + } + return res; +} + + +void QtLocalPeer::receiveConnection() +{ + QLocalSocket* socket = server->nextPendingConnection(); + if (!socket) + return; + + while (socket->bytesAvailable() < (int)sizeof(quint32)) + socket->waitForReadyRead(); + QDataStream ds(socket); + QByteArray uMsg; + quint32 remaining = 0; + ds >> remaining; + uMsg.resize(remaining); + int got = 0; + char* uMsgBuf = uMsg.data(); + do { + got = ds.readRawData(uMsgBuf, remaining); + remaining -= got; + uMsgBuf += got; + } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); + if (got < 0) { + qWarning("QtLocalPeer: Message reception failed %s", socket->errorString().toLatin1().constData()); + delete socket; + return; + } + QString message(QString::fromUtf8(uMsg)); + socket->write(ack, qstrlen(ack)); + socket->waitForBytesWritten(1000); + delete socket; + emit messageReceived(message); //### (might take a long time to return) +} diff --git a/core/qtsingleapplication/qtlocalpeer.h b/core/qtsingleapplication/qtlocalpeer.h new file mode 100644 index 0000000..7b3fa81 --- /dev/null +++ b/core/qtsingleapplication/qtlocalpeer.h @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#ifndef QTLOCALPEER_H +#define QTLOCALPEER_H + +#include +#include +#include + +#include "qtlockedfile.h" + +class QtLocalPeer : public QObject +{ + Q_OBJECT + +public: + QtLocalPeer(QObject *parent = 0, const QString &appId = QString()); + bool isClient(); + bool sendMessage(const QString &message, int timeout); + QString applicationId() const + { return id; } + +Q_SIGNALS: + void messageReceived(const QString &message); + +protected Q_SLOTS: + void receiveConnection(); + +protected: + QString id; + QString socketName; + QLocalServer* server; + QtLP_Private::QtLockedFile lockFile; + +private: + static const char* ack; +}; + +#endif // QTLOCALPEER_H diff --git a/core/qtsingleapplication/qtlockedfile.cpp b/core/qtsingleapplication/qtlockedfile.cpp new file mode 100644 index 0000000..7fdf487 --- /dev/null +++ b/core/qtsingleapplication/qtlockedfile.cpp @@ -0,0 +1,192 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#include "qtlockedfile.h" + +/*! + \class QtLockedFile + + \brief The QtLockedFile class extends QFile with advisory locking + functions. + + A file may be locked in read or write mode. Multiple instances of + \e QtLockedFile, created in multiple processes running on the same + machine, may have a file locked in read mode. Exactly one instance + may have it locked in write mode. A read and a write lock cannot + exist simultaneously on the same file. + + The file locks are advisory. This means that nothing prevents + another process from manipulating a locked file using QFile or + file system functions offered by the OS. Serialization is only + guaranteed if all processes that access the file use + QLockedFile. Also, while holding a lock on a file, a process + must not open the same file again (through any API), or locks + can be unexpectedly lost. + + The lock provided by an instance of \e QtLockedFile is released + whenever the program terminates. This is true even when the + program crashes and no destructors are called. +*/ + +/*! \enum QtLockedFile::LockMode + + This enum describes the available lock modes. + + \value ReadLock A read lock. + \value WriteLock A write lock. + \value NoLock Neither a read lock nor a write lock. +*/ + +/*! + Constructs an unlocked \e QtLockedFile object. This constructor + behaves in the same way as \e QFile::QFile(). + + \sa QFile::QFile() +*/ +QtLockedFile::QtLockedFile() + : QFile() +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Constructs an unlocked QtLockedFile object with file \a name. This + constructor behaves in the same way as \e QFile::QFile(const + QString&). + + \sa QFile::QFile() +*/ +QtLockedFile::QtLockedFile(const QString &name) + : QFile(name) +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Opens the file in OpenMode \a mode. + + This is identical to QFile::open(), with the one exception that the + Truncate mode flag is disallowed. Truncation would conflict with the + advisory file locking, since the file would be modified before the + write lock is obtained. If truncation is required, use resize(0) + after obtaining the write lock. + + Returns true if successful; otherwise false. + + \sa QFile::open(), QFile::resize() +*/ +bool QtLockedFile::open(OpenMode mode) +{ + if (mode & QIODevice::Truncate) { + qWarning("QtLockedFile::open(): Truncate mode not allowed."); + return false; + } + return QFile::open(mode); +} + +/*! + Returns \e true if this object has a in read or write lock; + otherwise returns \e false. + + \sa lockMode() +*/ +bool QtLockedFile::isLocked() const +{ + return m_lock_mode != NoLock; +} + +/*! + Returns the type of lock currently held by this object, or \e + QtLockedFile::NoLock. + + \sa isLocked() +*/ +QtLockedFile::LockMode QtLockedFile::lockMode() const +{ + return m_lock_mode; +} + +/*! + \fn bool QtLockedFile::lock(LockMode mode, bool block = true) + + Obtains a lock of type \a mode. The file must be opened before it + can be locked. + + If \a block is true, this function will block until the lock is + aquired. If \a block is false, this function returns \e false + immediately if the lock cannot be aquired. + + If this object already has a lock of type \a mode, this function + returns \e true immediately. If this object has a lock of a + different type than \a mode, the lock is first released and then a + new lock is obtained. + + This function returns \e true if, after it executes, the file is + locked by this object, and \e false otherwise. + + \sa unlock(), isLocked(), lockMode() +*/ + +/*! + \fn bool QtLockedFile::unlock() + + Releases a lock. + + If the object has no lock, this function returns immediately. + + This function returns \e true if, after it executes, the file is + not locked by this object, and \e false otherwise. + + \sa lock(), isLocked(), lockMode() +*/ + +/*! + \fn QtLockedFile::~QtLockedFile() + + Destroys the \e QtLockedFile object. If any locks were held, they + are released. +*/ diff --git a/core/qtsingleapplication/qtlockedfile.h b/core/qtsingleapplication/qtlockedfile.h new file mode 100644 index 0000000..08be78f --- /dev/null +++ b/core/qtsingleapplication/qtlockedfile.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#ifndef QTLOCKEDFILE_H +#define QTLOCKEDFILE_H + +#include +#ifdef Q_OS_WIN +#include +#endif + +#if defined(Q_WS_WIN) +# if !defined(QT_QTLOCKEDFILE_EXPORT) && !defined(QT_QTLOCKEDFILE_IMPORT) +# define QT_QTLOCKEDFILE_EXPORT +# elif defined(QT_QTLOCKEDFILE_IMPORT) +# if defined(QT_QTLOCKEDFILE_EXPORT) +# undef QT_QTLOCKEDFILE_EXPORT +# endif +# define QT_QTLOCKEDFILE_EXPORT __declspec(dllimport) +# elif defined(QT_QTLOCKEDFILE_EXPORT) +# undef QT_QTLOCKEDFILE_EXPORT +# define QT_QTLOCKEDFILE_EXPORT __declspec(dllexport) +# endif +#else +# define QT_QTLOCKEDFILE_EXPORT +#endif + +namespace QtLP_Private { + +class QT_QTLOCKEDFILE_EXPORT QtLockedFile : public QFile +{ +public: + enum LockMode { NoLock = 0, ReadLock, WriteLock }; + + QtLockedFile(); + QtLockedFile(const QString &name); + ~QtLockedFile(); + + bool open(OpenMode mode); + + bool lock(LockMode mode, bool block = true); + bool unlock(); + bool isLocked() const; + LockMode lockMode() const; + +private: +#ifdef Q_OS_WIN + Qt::HANDLE wmutex; + Qt::HANDLE rmutex; + QVector rmutexes; + QString mutexname; + + Qt::HANDLE getMutexHandle(int idx, bool doCreate); + bool waitMutex(Qt::HANDLE mutex, bool doBlock); + +#endif + LockMode m_lock_mode; +}; +} +#endif diff --git a/core/qtsingleapplication/qtlockedfile_unix.cpp b/core/qtsingleapplication/qtlockedfile_unix.cpp new file mode 100644 index 0000000..715c7d9 --- /dev/null +++ b/core/qtsingleapplication/qtlockedfile_unix.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#include +#include +#include +#include + +#include "qtlockedfile.h" + +bool QtLockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK; + int cmd = block ? F_SETLKW : F_SETLK; + int ret = fcntl(handle(), cmd, &fl); + + if (ret == -1) { + if (errno != EINTR && errno != EAGAIN) + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + + m_lock_mode = mode; + return true; +} + + +bool QtLockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = F_UNLCK; + int ret = fcntl(handle(), F_SETLKW, &fl); + + if (ret == -1) { + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + m_lock_mode = NoLock; + return true; +} + +QtLockedFile::~QtLockedFile() +{ + if (isOpen()) + unlock(); +} + diff --git a/core/qtsingleapplication/qtlockedfile_win.cpp b/core/qtsingleapplication/qtlockedfile_win.cpp new file mode 100644 index 0000000..b54ac34 --- /dev/null +++ b/core/qtsingleapplication/qtlockedfile_win.cpp @@ -0,0 +1,204 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#include "qtlockedfile.h" +#include +#include + +#define MUTEX_PREFIX "QtLockedFile mutex " +// Maximum number of concurrent read locks. Must not be greater than MAXIMUM_WAIT_OBJECTS +#define MAX_READERS MAXIMUM_WAIT_OBJECTS + +Qt::HANDLE QtLockedFile::getMutexHandle(int idx, bool doCreate) +{ + if (mutexname.isEmpty()) { + QFileInfo fi(*this); + mutexname = QString::fromLatin1(MUTEX_PREFIX) + + fi.absoluteFilePath().toLower(); + } + QString mname(mutexname); + if (idx >= 0) + mname += QString::number(idx); + + Qt::HANDLE mutex; + if (doCreate) { + mutex = CreateMutexW(NULL, FALSE, (TCHAR*)mname.utf16()); + if (!mutex) { + qErrnoWarning("QtLockedFile::lock(): CreateMutex failed"); + return 0; + } + } + else { + mutex = OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, (TCHAR*)mname.utf16()); + if (!mutex) { + if (GetLastError() != ERROR_FILE_NOT_FOUND) + qErrnoWarning("QtLockedFile::lock(): OpenMutex failed"); + return 0; + } + } + return mutex; +} + +bool QtLockedFile::waitMutex(Qt::HANDLE mutex, bool doBlock) +{ + Q_ASSERT(mutex); + DWORD res = WaitForSingleObject(mutex, doBlock ? INFINITE : 0); + switch (res) { + case WAIT_OBJECT_0: + case WAIT_ABANDONED: + return true; + break; + case WAIT_TIMEOUT: + break; + default: + qErrnoWarning("QtLockedFile::lock(): WaitForSingleObject failed"); + } + return false; +} + + + +bool QtLockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + if (!wmutex && !(wmutex = getMutexHandle(-1, true))) + return false; + + if (!waitMutex(wmutex, block)) + return false; + + if (mode == ReadLock) { + int idx = 0; + for (; idx < MAX_READERS; idx++) { + rmutex = getMutexHandle(idx, false); + if (!rmutex || waitMutex(rmutex, false)) + break; + CloseHandle(rmutex); + } + bool ok = true; + if (idx >= MAX_READERS) { + qWarning("QtLockedFile::lock(): too many readers"); + rmutex = 0; + ok = false; + } + else if (!rmutex) { + rmutex = getMutexHandle(idx, true); + if (!rmutex || !waitMutex(rmutex, false)) + ok = false; + } + if (!ok && rmutex) { + CloseHandle(rmutex); + rmutex = 0; + } + ReleaseMutex(wmutex); + if (!ok) + return false; + } + else { + Q_ASSERT(rmutexes.isEmpty()); + for (int i = 0; i < MAX_READERS; i++) { + Qt::HANDLE mutex = getMutexHandle(i, false); + if (mutex) + rmutexes.append(mutex); + } + if (rmutexes.size()) { + DWORD res = WaitForMultipleObjects(rmutexes.size(), rmutexes.constData(), + TRUE, block ? INFINITE : 0); + if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED) { + if (res != WAIT_TIMEOUT) + qErrnoWarning("QtLockedFile::lock(): WaitForMultipleObjects failed"); + m_lock_mode = WriteLock; // trick unlock() to clean up - semiyucky + unlock(); + return false; + } + } + } + + m_lock_mode = mode; + return true; +} + +bool QtLockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + if (m_lock_mode == ReadLock) { + ReleaseMutex(rmutex); + CloseHandle(rmutex); + rmutex = 0; + } + else { + foreach(Qt::HANDLE mutex, rmutexes) { + ReleaseMutex(mutex); + CloseHandle(mutex); + } + rmutexes.clear(); + ReleaseMutex(wmutex); + } + + m_lock_mode = QtLockedFile::NoLock; + return true; +} + +QtLockedFile::~QtLockedFile() +{ + if (isOpen()) + unlock(); + if (wmutex) + CloseHandle(wmutex); +} diff --git a/core/qtsingleapplication/qtsingleapplication.cpp b/core/qtsingleapplication/qtsingleapplication.cpp new file mode 100644 index 0000000..3cc0682 --- /dev/null +++ b/core/qtsingleapplication/qtsingleapplication.cpp @@ -0,0 +1,331 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + + +#include "qtsingleapplication.h" +#include "qtlocalpeer.h" +#include + + +/*! + \class QtSingleApplication qtsingleapplication.h + \brief The QtSingleApplication class provides an API to detect and + communicate with running instances of an application. + + This class allows you to create applications where only one + instance should be running at a time. I.e., if the user tries to + launch another instance, the already running instance will be + activated instead. Another usecase is a client-server system, + where the first started instance will assume the role of server, + and the later instances will act as clients of that server. + + By default, the full path of the executable file is used to + determine whether two processes are instances of the same + application. You can also provide an explicit identifier string + that will be compared instead. + + The application should create the QtSingleApplication object early + in the startup phase, and call isRunning() to find out if another + instance of this application is already running. If isRunning() + returns false, it means that no other instance is running, and + this instance has assumed the role as the running instance. In + this case, the application should continue with the initialization + of the application user interface before entering the event loop + with exec(), as normal. + + The messageReceived() signal will be emitted when the running + application receives messages from another instance of the same + application. When a message is received it might be helpful to the + user to raise the application so that it becomes visible. To + facilitate this, QtSingleApplication provides the + setActivationWindow() function and the activateWindow() slot. + + If isRunning() returns true, another instance is already + running. It may be alerted to the fact that another instance has + started by using the sendMessage() function. Also data such as + startup parameters (e.g. the name of the file the user wanted this + new instance to open) can be passed to the running instance with + this function. Then, the application should terminate (or enter + client mode). + + If isRunning() returns true, but sendMessage() fails, that is an + indication that the running instance is frozen. + + Here's an example that shows how to convert an existing + application to use QtSingleApplication. It is very simple and does + not make use of all QtSingleApplication's functionality (see the + examples for that). + + \code + // Original + int main(int argc, char **argv) + { + QApplication app(argc, argv); + + MyMainWidget mmw; + mmw.show(); + return app.exec(); + } + + // Single instance + int main(int argc, char **argv) + { + QtSingleApplication app(argc, argv); + + if (app.isRunning()) + return !app.sendMessage(someDataString); + + MyMainWidget mmw; + app.setActivationWindow(&mmw); + mmw.show(); + return app.exec(); + } + \endcode + + Once this QtSingleApplication instance is destroyed (normally when + the process exits or crashes), when the user next attempts to run the + application this instance will not, of course, be encountered. The + next instance to call isRunning() or sendMessage() will assume the + role as the new running instance. + + For console (non-GUI) applications, QtSingleCoreApplication may be + used instead of this class, to avoid the dependency on the QtGui + library. + + \sa QtSingleCoreApplication +*/ + + +void QtSingleApplication::sysInit(const QString &appId) +{ + actWin = 0; + peer = new QtLocalPeer(this, appId); + connect(peer, SIGNAL(messageReceived(const QString&)), SIGNAL(messageReceived(const QString&))); +} + + +/*! + Creates a QtSingleApplication object. The application identifier + will be QCoreApplication::applicationFilePath(). \a argc, \a + argv, and \a GUIenabled are passed on to the QAppliation constructor. + + If you are creating a console application (i.e. setting \a + GUIenabled to false), you may consider using + QtSingleCoreApplication instead. +*/ + +QtSingleApplication::QtSingleApplication(int &argc, char **argv, bool GUIenabled) + : QApplication(argc, argv, GUIenabled) +{ + sysInit(); +} + + +/*! + Creates a QtSingleApplication object with the application + identifier \a appId. \a argc and \a argv are passed on to the + QAppliation constructor. +*/ + +QtSingleApplication::QtSingleApplication(const QString &appId, int &argc, char **argv) + : QApplication(argc, argv) +{ + sysInit(appId); +} + +#if defined(Q_WS_X11) +/*! + Special constructor for X11, ref. the documentation of + QApplication's corresponding constructor. The application identifier + will be QCoreApplication::applicationFilePath(). \a dpy, \a visual, + and \a cmap are passed on to the QApplication constructor. +*/ +QtSingleApplication::QtSingleApplication(Display* dpy, Qt::HANDLE visual, Qt::HANDLE cmap) + : QApplication(dpy, visual, cmap) +{ + sysInit(); +} + +/*! + Special constructor for X11, ref. the documentation of + QApplication's corresponding constructor. The application identifier + will be QCoreApplication::applicationFilePath(). \a dpy, \a argc, \a + argv, \a visual, and \a cmap are passed on to the QApplication + constructor. +*/ +QtSingleApplication::QtSingleApplication(Display *dpy, int &argc, char **argv, Qt::HANDLE visual, Qt::HANDLE cmap) + : QApplication(dpy, argc, argv, visual, cmap) +{ + sysInit(); +} + +/*! + Special constructor for X11, ref. the documentation of + QApplication's corresponding constructor. The application identifier + will be \a appId. \a dpy, \a argc, \a + argv, \a visual, and \a cmap are passed on to the QApplication + constructor. +*/ +QtSingleApplication::QtSingleApplication(Display* dpy, const QString &appId, int argc, char **argv, Qt::HANDLE visual, Qt::HANDLE cmap) + : QApplication(dpy, argc, argv, visual, cmap) +{ + sysInit(appId); +} +#endif + + +/*! + Returns true if another instance of this application is running; + otherwise false. + + This function does not find instances of this application that are + being run by a different user (on Windows: that are running in + another session). + + \sa sendMessage() +*/ + +bool QtSingleApplication::isRunning() +{ + return peer->isClient(); +} + + +/*! + Tries to send the text \a message to the currently running + instance. The QtSingleApplication object in the running instance + will emit the messageReceived() signal when it receives the + message. + + This function returns true if the message has been sent to, and + processed by, the current instance. If there is no instance + currently running, or if the running instance fails to process the + message within \a timeout milliseconds, this function return false. + + \sa isRunning(), messageReceived() +*/ +bool QtSingleApplication::sendMessage(const QString &message, int timeout) +{ + return peer->sendMessage(message, timeout); +} + + +/*! + Returns the application identifier. Two processes with the same + identifier will be regarded as instances of the same application. +*/ +QString QtSingleApplication::id() const +{ + return peer->applicationId(); +} + + +/*! + Sets the activation window of this application to \a aw. The + activation window is the widget that will be activated by + activateWindow(). This is typically the application's main window. + + If \a activateOnMessage is true (the default), the window will be + activated automatically every time a message is received, just prior + to the messageReceived() signal being emitted. + + \sa activateWindow(), messageReceived() +*/ + +void QtSingleApplication::setActivationWindow(QWidget* aw, bool activateOnMessage) +{ + actWin = aw; + if (activateOnMessage) + connect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow())); + else + disconnect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow())); +} + + +/*! + Returns the applications activation window if one has been set by + calling setActivationWindow(), otherwise returns 0. + + \sa setActivationWindow() +*/ +QWidget* QtSingleApplication::activationWindow() const +{ + return actWin; +} + + +/*! + De-minimizes, raises, and activates this application's activation window. + This function does nothing if no activation window has been set. + + This is a convenience function to show the user that this + application instance has been activated when he has tried to start + another instance. + + This function should typically be called in response to the + messageReceived() signal. By default, that will happen + automatically, if an activation window has been set. + + \sa setActivationWindow(), messageReceived(), initialize() +*/ +void QtSingleApplication::activateWindow() +{ + if (actWin) { + actWin->setWindowState(actWin->windowState() & ~Qt::WindowMinimized); + actWin->raise(); + actWin->activateWindow(); + } +} + + +/*! + \fn void QtSingleApplication::messageReceived(const QString& message) + + This signal is emitted when the current instance receives a \a + message from another instance of this application. + + \sa sendMessage(), setActivationWindow(), activateWindow() +*/ + + +/*! + \fn void QtSingleApplication::initialize(bool dummy = true) + + \obsolete +*/ diff --git a/core/qtsingleapplication/qtsingleapplication.h b/core/qtsingleapplication/qtsingleapplication.h new file mode 100644 index 0000000..3ed62d5 --- /dev/null +++ b/core/qtsingleapplication/qtsingleapplication.h @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#ifndef QTSINGLEAPPLICATION_H +#define QTSINGLEAPPLICATION_H + +#include + +class QtLocalPeer; + +#if defined(Q_WS_WIN) +# if !defined(QT_QTSINGLEAPPLICATION_EXPORT) && !defined(QT_QTSINGLEAPPLICATION_IMPORT) +# define QT_QTSINGLEAPPLICATION_EXPORT +# elif defined(QT_QTSINGLEAPPLICATION_IMPORT) +# if defined(QT_QTSINGLEAPPLICATION_EXPORT) +# undef QT_QTSINGLEAPPLICATION_EXPORT +# endif +# define QT_QTSINGLEAPPLICATION_EXPORT __declspec(dllimport) +# elif defined(QT_QTSINGLEAPPLICATION_EXPORT) +# undef QT_QTSINGLEAPPLICATION_EXPORT +# define QT_QTSINGLEAPPLICATION_EXPORT __declspec(dllexport) +# endif +#else +# define QT_QTSINGLEAPPLICATION_EXPORT +#endif + +class QT_QTSINGLEAPPLICATION_EXPORT QtSingleApplication : public QApplication +{ + Q_OBJECT + +public: + QtSingleApplication(int &argc, char **argv, bool GUIenabled = true); + QtSingleApplication(const QString &id, int &argc, char **argv); +#if defined(Q_WS_X11) + QtSingleApplication(Display* dpy, Qt::HANDLE visual = 0, Qt::HANDLE colormap = 0); + QtSingleApplication(Display *dpy, int &argc, char **argv, Qt::HANDLE visual = 0, Qt::HANDLE cmap= 0); + QtSingleApplication(Display* dpy, const QString &appId, int argc, char **argv, Qt::HANDLE visual = 0, Qt::HANDLE colormap = 0); +#endif + + bool isRunning(); + QString id() const; + + void setActivationWindow(QWidget* aw, bool activateOnMessage = true); + QWidget* activationWindow() const; + + // Obsolete: + void initialize(bool dummy = true) + { isRunning(); Q_UNUSED(dummy) } + +public Q_SLOTS: + bool sendMessage(const QString &message, int timeout = 5000); + void activateWindow(); + + +Q_SIGNALS: + void messageReceived(const QString &message); + + +private: + void sysInit(const QString &appId = QString()); + QtLocalPeer *peer; + QWidget *actWin; +}; + +#endif // QTSINGLEAPPLICATION_H diff --git a/core/qtsingleapplication/qtsingleapplication.pri b/core/qtsingleapplication/qtsingleapplication.pri new file mode 100644 index 0000000..273ecb9 --- /dev/null +++ b/core/qtsingleapplication/qtsingleapplication.pri @@ -0,0 +1,6 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD +QT *= network + +SOURCES += $$PWD/qtsingleapplication.cpp $$PWD/qtlocalpeer.cpp +HEADERS += $$PWD/qtsingleapplication.h $$PWD/qtlocalpeer.h diff --git a/core/qtsingleapplication/qtsinglecoreapplication.cpp b/core/qtsingleapplication/qtsinglecoreapplication.cpp new file mode 100644 index 0000000..cf60771 --- /dev/null +++ b/core/qtsingleapplication/qtsinglecoreapplication.cpp @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + + +#include "qtsinglecoreapplication.h" +#include "qtlocalpeer.h" + +/*! + \class QtSingleCoreApplication qtsinglecoreapplication.h + \brief A variant of the QtSingleApplication class for non-GUI applications. + + This class is a variant of QtSingleApplication suited for use in + console (non-GUI) applications. It is an extension of + QCoreApplication (instead of QApplication). It does not require + the QtGui library. + + The API and usage is identical to QtSingleApplication, except that + functions relating to the "activation window" are not present, for + obvious reasons. Please refer to the QtSingleApplication + documentation for explanation of the usage. + + A QtSingleCoreApplication instance can communicate to a + QtSingleApplication instance if they share the same application + id. Hence, this class can be used to create a light-weight + command-line tool that sends commands to a GUI application. + + \sa QtSingleApplication +*/ + +/*! + Creates a QtSingleCoreApplication object. The application identifier + will be QCoreApplication::applicationFilePath(). \a argc and \a + argv are passed on to the QCoreAppliation constructor. +*/ + +QtSingleCoreApplication::QtSingleCoreApplication(int &argc, char **argv) + : QCoreApplication(argc, argv) +{ + peer = new QtLocalPeer(this); + connect(peer, SIGNAL(messageReceived(const QString&)), SIGNAL(messageReceived(const QString&))); +} + + +/*! + Creates a QtSingleCoreApplication object with the application + identifier \a appId. \a argc and \a argv are passed on to the + QCoreAppliation constructor. +*/ +QtSingleCoreApplication::QtSingleCoreApplication(const QString &appId, int &argc, char **argv) + : QCoreApplication(argc, argv) +{ + peer = new QtLocalPeer(this, appId); + connect(peer, SIGNAL(messageReceived(const QString&)), SIGNAL(messageReceived(const QString&))); +} + + +/*! + Returns true if another instance of this application is running; + otherwise false. + + This function does not find instances of this application that are + being run by a different user (on Windows: that are running in + another session). + + \sa sendMessage() +*/ + +bool QtSingleCoreApplication::isRunning() +{ + return peer->isClient(); +} + + +/*! + Tries to send the text \a message to the currently running + instance. The QtSingleCoreApplication object in the running instance + will emit the messageReceived() signal when it receives the + message. + + This function returns true if the message has been sent to, and + processed by, the current instance. If there is no instance + currently running, or if the running instance fails to process the + message within \a timeout milliseconds, this function return false. + + \sa isRunning(), messageReceived() +*/ + +bool QtSingleCoreApplication::sendMessage(const QString &message, int timeout) +{ + return peer->sendMessage(message, timeout); +} + + +/*! + Returns the application identifier. Two processes with the same + identifier will be regarded as instances of the same application. +*/ + +QString QtSingleCoreApplication::id() const +{ + return peer->applicationId(); +} + + +/*! + \fn void QtSingleCoreApplication::messageReceived(const QString& message) + + This signal is emitted when the current instance receives a \a + message from another instance of this application. + + \sa sendMessage() +*/ diff --git a/core/qtsingleapplication/qtsinglecoreapplication.h b/core/qtsingleapplication/qtsinglecoreapplication.h new file mode 100644 index 0000000..7cde4b8 --- /dev/null +++ b/core/qtsingleapplication/qtsinglecoreapplication.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of a Qt Solutions component. +** +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +** the names of its contributors may be used to endorse or promote +** products derived from this software without specific prior written +** permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +****************************************************************************/ + +#ifndef QTSINGLECOREAPPLICATION_H +#define QTSINGLECOREAPPLICATION_H + +#include + +class QtLocalPeer; + +class QtSingleCoreApplication : public QCoreApplication +{ + Q_OBJECT + +public: + QtSingleCoreApplication(int &argc, char **argv); + QtSingleCoreApplication(const QString &id, int &argc, char **argv); + + bool isRunning(); + QString id() const; + +public Q_SLOTS: + bool sendMessage(const QString &message, int timeout = 5000); + + +Q_SIGNALS: + void messageReceived(const QString &message); + + +private: + QtLocalPeer* peer; +}; + +#endif // QTSINGLECOREAPPLICATION_H diff --git a/core/qtsingleapplication/qtsinglecoreapplication.pri b/core/qtsingleapplication/qtsinglecoreapplication.pri new file mode 100644 index 0000000..d2d6cc3 --- /dev/null +++ b/core/qtsingleapplication/qtsinglecoreapplication.pri @@ -0,0 +1,10 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD +HEADERS += $$PWD/qtsinglecoreapplication.h $$PWD/qtlocalpeer.h +SOURCES += $$PWD/qtsinglecoreapplication.cpp $$PWD/qtlocalpeer.cpp + +QT *= network + +win32:contains(TEMPLATE, lib):contains(CONFIG, shared) { + DEFINES += QT_QTSINGLECOREAPPLICATION_EXPORT=__declspec(dllexport) +} diff --git a/core/timeformatter.cpp b/core/timeformatter.cpp new file mode 100644 index 0000000..af57d22 --- /dev/null +++ b/core/timeformatter.cpp @@ -0,0 +1,10 @@ +#include "timeformatter.h" + +#include + +TimeFormatter::TimeFormatter(QObject *parent) : QObject(parent) {} + +QString TimeFormatter::format(double seconds) { + return QTime::fromMSecsSinceStartOfDay(static_cast(seconds * 1000)) + .toString("hh:mm:ss.zzz"); +} diff --git a/core/timeformatter.h b/core/timeformatter.h new file mode 100644 index 0000000..7f0d602 --- /dev/null +++ b/core/timeformatter.h @@ -0,0 +1,15 @@ +#ifndef TIMEFORMATTER_H +#define TIMEFORMATTER_H + +#include + +class TimeFormatter : public QObject { + Q_OBJECT +public: + explicit TimeFormatter(QObject *parent = 0); + +public slots: + QString format(double seconds); +}; + +#endif // TIMEFORMATTER_H diff --git a/core/videoelement.cpp b/core/videoelement.cpp new file mode 100644 index 0000000..05b40fb --- /dev/null +++ b/core/videoelement.cpp @@ -0,0 +1,68 @@ +#include "videoelement.h" +#include "player.h" + +VideoElement::VideoElement() : m_source{nullptr} { + connect(this, SIGNAL(updateRequested()), this, SLOT(update()), + Qt::QueuedConnection); +} + +VideoElement::~VideoElement() {} + +VideoElement::Renderer *VideoElement::createRenderer() const { + qDebug("creating VideoElement::Renderer"); + return new Renderer; +} + +Player *VideoElement::source() const { return m_source; } + +void VideoElement::setSource(Player *source) { + if (m_source == source) + return; + + if (source) { + if (source->hasRenderer()) + return; + + PlayerRendererInterface *pri = source; + pri->rendererSinkSet(this); + + m_source = source; + + emit sourceChanged(source); + qDebug() << "source has been set!"; + rendererUpdateRequired = true; + return; + } + + PlayerRendererInterface *pri = m_source; + pri->rendererSinkSet(nullptr); + m_source = nullptr; + rendererUpdateRequired = true; +} + +VideoElement::Renderer::~Renderer() { delete m_renderer; } + +void VideoElement::Renderer::render() { + if (m_renderer) + m_renderer->render(framebufferObject()); +} + +void VideoElement::Renderer::synchronize(QQuickFramebufferObject *object) { + auto ve = static_cast(object); + if (!ve->rendererUpdateRequired) + return; + ve->rendererUpdateRequired = false; + if (!ve->source()) { + delete m_renderer; + m_renderer = nullptr; + return; + } + qDebug("creating backend renderer"); + m_renderer = ve->source()->backend()->createRenderer(ve); + // Call via base to ensure a public method. + PlayerRendererInterface *src = ve->m_source; + src->rendererReady(); + qDebug("backend renderer created"); +} + +void VideoElement::videoUpdated() { emit updateRequested(); } diff --git a/core/videoelement.h b/core/videoelement.h new file mode 100644 index 0000000..a26f512 --- /dev/null +++ b/core/videoelement.h @@ -0,0 +1,48 @@ +#ifndef VIDEOELEMENT_H +#define VIDEOELEMENT_H + +#include + +#include "player.h" +#include "playerplugininterface.h" + +class VideoElement : public QQuickFramebufferObject, + public VideoUpdateInterface { + Q_OBJECT + Q_PROPERTY(Player *source READ source WRITE setSource NOTIFY sourceChanged) + + class Renderer : public QQuickFramebufferObject::Renderer { + public: + ~Renderer(); + void render() override; + void synchronize(QQuickFramebufferObject *object) override; + + private: + VideoRendererBase *m_renderer; + }; + + Player *m_source; + +public: + VideoElement(); + ~VideoElement(); + + Renderer *createRenderer() const override; + Player *source() const; + +public slots: + void setSource(Player *source); + +signals: + void updateRequested(); + + void sourceChanged(Player *source); + +protected: + void videoUpdated() override; + +private: + bool rendererUpdateRequired = false; +}; + +#endif // VIDEOELEMENT_H diff --git a/resource/aniplayer-mikuru.ico b/resource/aniplayer-mikuru.ico new file mode 100644 index 0000000..2439682 Binary files /dev/null and b/resource/aniplayer-mikuru.ico differ diff --git a/resource/mikuru-icon-base.png b/resource/mikuru-icon-base.png new file mode 100644 index 0000000..7c362f8 Binary files /dev/null and b/resource/mikuru-icon-base.png differ diff --git a/resource/mikuru-icon-original.png b/resource/mikuru-icon-original.png new file mode 100644 index 0000000..7c0c662 Binary files /dev/null and b/resource/mikuru-icon-original.png differ