├── .gitignore ├── CMakeLists.txt ├── README.md └── src ├── Globals.cpp ├── Globals.h ├── ProgressContainerLayout.cpp ├── ProgressContainerLayout.h ├── ProgressDockWidget.cpp ├── ProgressDockWidget.h ├── ProgressSlider.cpp ├── ProgressSlider.h ├── SliderStyle.cpp ├── SliderStyle.h └── obs-progress.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /build/ 4 | /build32/ 5 | /build64/ 6 | /release/ 7 | /package/ 8 | /installer/Output/ 9 | .idea 10 | .vscode 11 | .vs 12 | CMakeFiles 13 | CMakeCache.txt 14 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # obs-progress/CMakeLists.txt 2 | 3 | cmake_minimum_required(VERSION 3.5) 4 | project(obs-progress) 5 | 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTOUIC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(LibObs REQUIRED) 13 | find_package(date CONFIG REQUIRED) 14 | find_package(Qt6 REQUIRED COMPONENTS Core Widgets) 15 | 16 | 17 | set(obs-progress_SOURCES 18 | src/obs-progress.cpp 19 | src/ProgressDockWidget.cpp 20 | src/ProgressContainerLayout.cpp 21 | src/Globals.cpp 22 | src/ProgressSlider.cpp 23 | src/SliderStyle.cpp 24 | ) 25 | 26 | set(obs-progress_HEADERS 27 | src/ProgressDockWidget.h 28 | src/ProgressContainerLayout.h 29 | src/Globals.h 30 | src/ProgressSlider.h 31 | src/SliderStyle.h 32 | ) 33 | 34 | add_library(obs-progress MODULE 35 | ${obs-progress_SOURCES} 36 | ${obs-progress_HEADERS} 37 | ) 38 | 39 | include_directories( 40 | ${OBS_LIBOBS_INCLUDE} 41 | ${OBS_FRONTEND_INCLUDE}) 42 | 43 | target_link_libraries(obs-progress 44 | ${OBS_LIB_DIR} 45 | Qt6::Core 46 | Qt6::Widgets) 47 | 48 | if(WIN32) 49 | if(NOT DEFINED OBS_FRONTEND_LIB) 50 | set(OBS_FRONTEND_LIB "OBS_FRONTEND_LIB-NOTFOUND" CACHE FILEPATH "OBS frontend library") 51 | message(FATAL_ERROR "Could not find OBS Frontend API's library !") 52 | endif() 53 | 54 | target_link_libraries(obs-progress 55 | "${OBS_FRONTEND_LIB}") 56 | 57 | # --- Release package helper --- 58 | # The "release" folder has a structure similar OBS' one on Windows 59 | set(RELEASE_DIR "${PROJECT_SOURCE_DIR}/release") 60 | 61 | endif() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obs-progress 2 | 3 | A plugin for OBS Studio that provides playback controls and progress indicators for media and slideshow sources. The controls are available in a standard OBS dock that can be floated, pinned, hidden, etc. 4 | 5 | ![screenshot](https://i.imgur.com/CTxWdmG.png) 6 | 7 | The following media indicators and controls are shown for all applicable sources in the active scene. 8 | - Media 9 | - Play/pause/stop 10 | - Toggle loop option 11 | - Click progress bar or click and drag playhead to seek through media 12 | - Elapsed/total/remaining time 13 | - Slideshows 14 | - Next/previous/restart 15 | 16 | ![screenshot](https://i.imgur.com/y1iwCVY.png) 17 | 18 | Here are the controls pinned to the UI. The title of the dock displays the scene name and the text above the controls shows the source name. In this case, the active scene contains a slideshow and a media source. 19 | 20 | # How to Use 21 | 22 | Choose one of the following releases. 23 | 24 | * Use the [latest-qt5](https://github.com/micahmo/obs-progress/releases/tag/latest-qt5) release for OBS < 28.0. 25 | * Use the [latest-qt6](https://github.com/micahmo/obs-progress/releases/tag/latest-qt6) release for OBS >= 28.0. 26 | 27 | From the release, download `obs-progress.dll`. Place the file in OBS's 64-bit Plugins directory (for example, `C:\Program Files\obs-studio\obs-plugins\64bit`). 28 | 29 | Start OBS to use. The progress may initially appear undocked. It can be repositioned manually and its size/position/state will be restored from session to session. 30 | 31 | # FAQ 32 | 33 | #### OBS added Media Controls to [OBS Studio 26.0](https://obsproject.com/forum/threads/obs-studio-26-0-release-candidate.129075/). Why is an additional plugin needed? 34 | The built-in media controls are *very* limited. Namely, the controls are only shown for the currently selected source. This has several drawbacks. 35 | - You must manually select the source before seeing the media controls. 36 | - If there are multiple media sources in a given scene, you can only see the media controls for one at a time. 37 | - If you are using Studio Mode, the Preview and Program windows will be different, so the selected source will never be the active one. 38 | 39 | #### Why not use the existing [Media Controls](https://obsproject.com/forum/resources/media-controls.1032/) plugin? 40 | Please do! It is a great plugin with good support. In fact, its code was incorporated into the official feature in OBS. 41 | 42 | # OBS Studio Issues 43 | 44 | There are no known issues with the latest versions of OBS. 45 | 46 | # Development 47 | 48 | Install Visual Studio with the "Desktop development with C++" option enabled. 49 | 50 | Download and run the [CMake Windows x64 Installer](https://cmake.org/download/). 51 | 52 | Clone [vcpkg](https://vcpkg.io/en/getting-started.html). Initialize with `.\bootstrap-vcpkg.bat`. Run `.\vcpkg.exe install date --triplet x64-windows` and `.\vcpkg integrate install`. 53 | 54 | Clone the [OBS Studio](https://github.com/obsproject/obs-studio) repo (with `--recursive`). Run `CI/build-windows.ps1` to do a Release build and `CI/build-windows.ps1 -BuildConfiguration Debug` to do a Debug build. 55 | 56 | Clone this repository. Update the paths in following command to reference your cloned repositories. Then navigate to the root of this repo and run it. 57 | 58 | ``` powershell 59 | cmake . -B build ` 60 | -D LibObs_DIR="\obs-studio\build64\libobs\" ` 61 | -D w32-pthreads_DIR="\obs-studio\build64\deps\w32-pthreads\" ` 62 | -D OBS_FRONTEND_LIB="\obs-studio\build64\UI\obs-frontend-api\Debug\obs-frontend-api.lib" ` 63 | -D OBS_LIBOBS_INCLUDE="\obs-studio\libobs\" ` 64 | -D OBS_FRONTEND_INCLUDE="\obs-studio\UI\obs-frontend-api\" ` 65 | -D OBS_LIB_DIR="\obs-studio\build64\libobs\Debug\obs.lib" ` 66 | -D date_DIR="\vcpkg\installed\x64-windows\share\date\" ` 67 | -D Qt6_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6\" ` 68 | -D Qt6Core_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6Core\" ` 69 | -D Qt6Widgets_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6Widgets\" ` 70 | -D Qt6WidgetsTools_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6WidgetsTools\" ` 71 | -D Qt6CoreTools_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6CoreTools\" ` 72 | -D Qt6GuiTools_DIR="\obs-build-dependencies\windows-deps-2023-03-04-x64\lib\cmake\Qt6GuiTools\" 73 | ``` 74 | 75 | Open the resulting `build\obs-progress.sln` in Visual Studio. 76 | 77 | ### Debug 78 | 79 | Build the solution with the `Debug` configuration selected. Copy the output from `build\Debug\obs-progress.dll` to `obs-studio\build64\rundir\Debug\obs-plugins\64bit`. 80 | 81 | Run the debug version of OBS from `obs-studio\build64\rundir\Debug\bin\64bit\obs64.exe`. It should then be possible to attach from Visual Studio to the running OBS to debug the plugin. 82 | 83 | ### Release 84 | 85 | Build the solution with the `Release` configuration selected. Copy the output from `build\Release\obs-progress.dll` to `C:\Program Files\obs-studio\obs-plugins\64bit`. 86 | 87 | Run OBS from `C:\Program Files\obs-studio\bin\64bit\obs64.exe` for final testing. 88 | -------------------------------------------------------------------------------- /src/Globals.cpp: -------------------------------------------------------------------------------- 1 | #include "Globals.h" 2 | #include 3 | 4 | QIcon Globals::pauseIcon; 5 | QIcon Globals::playIcon; 6 | QIcon Globals::stopIcon; 7 | QIcon Globals::loopIcon; 8 | QIcon Globals::previousIcon; 9 | QIcon Globals::nextIcon; 10 | QIcon Globals::restartIcon; 11 | 12 | void Globals::initialize() 13 | { 14 | QPixmap pauseImage(":/qt-project.org/styles/commonstyle/images/media-pause-16.png"); 15 | QPainter pauseImagePainter(&pauseImage); 16 | pauseImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 17 | pauseImagePainter.fillRect(pauseImage.rect(), Qt::gray); 18 | pauseImagePainter.end(); 19 | Globals::pauseIcon = QIcon(pauseImage); 20 | 21 | QPixmap playImage(":/qt-project.org/styles/commonstyle/images/media-play-16.png"); 22 | QPainter playImagePainter(&playImage); 23 | playImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 24 | playImagePainter.fillRect(playImage.rect(), Qt::gray); 25 | playImagePainter.end(); 26 | Globals::playIcon = QIcon(playImage); 27 | 28 | QPixmap stopImage(":/qt-project.org/styles/commonstyle/images/media-stop-16.png"); 29 | QPainter stopImagePainter(&stopImage); 30 | stopImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 31 | stopImagePainter.fillRect(stopImage.rect(), Qt::gray); 32 | stopImagePainter.end(); 33 | Globals::stopIcon = QIcon(stopImage); 34 | 35 | QPixmap loopImage(":/qt-project.org/styles/commonstyle/images/refresh-24.png"); 36 | QPainter loopImagePainter(&loopImage); 37 | loopImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 38 | loopImagePainter.fillRect(loopImage.rect(), Qt::lightGray); 39 | loopImagePainter.end(); 40 | Globals::loopIcon = QIcon(loopImage); 41 | 42 | QPixmap previousImage(":/qt-project.org/styles/commonstyle/images/media-seek-backward-16.png"); 43 | QPainter previousImagePainter(&previousImage); 44 | previousImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 45 | previousImagePainter.fillRect(previousImage.rect(), Qt::lightGray); 46 | previousImagePainter.end(); 47 | Globals::previousIcon = QIcon(previousImage); 48 | 49 | QPixmap nextImage(":/qt-project.org/styles/commonstyle/images/media-seek-forward-16.png"); 50 | QPainter nextImagePainter(&nextImage); 51 | nextImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 52 | nextImagePainter.fillRect(nextImage.rect(), Qt::lightGray); 53 | nextImagePainter.end(); 54 | Globals::nextIcon = QIcon(nextImage); 55 | 56 | QPixmap restartImage(":/qt-project.org/styles/commonstyle/images/media-skip-backward-16.png"); 57 | QPainter restartImagePainter(&restartImage); 58 | restartImagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); 59 | restartImagePainter.fillRect(restartImage.rect(), Qt::lightGray); 60 | restartImagePainter.end(); 61 | Globals::restartIcon = QIcon(restartImage); 62 | } -------------------------------------------------------------------------------- /src/Globals.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class Globals 4 | { 5 | public: 6 | static void initialize(); 7 | static QIcon pauseIcon; 8 | static QIcon playIcon; 9 | static QIcon stopIcon; 10 | static QIcon loopIcon; 11 | static QIcon previousIcon; 12 | static QIcon nextIcon; 13 | static QIcon restartIcon; 14 | }; -------------------------------------------------------------------------------- /src/ProgressContainerLayout.cpp: -------------------------------------------------------------------------------- 1 | #include "ProgressContainerLayout.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "Globals.h" 7 | #include 8 | #include 9 | 10 | #include "ProgressSlider.h" 11 | 12 | ProgressContainerLayout::ProgressContainerLayout(QWidget* parent) 13 | : QVBoxLayout(parent) 14 | { 15 | addStretch(); 16 | addStretch(); 17 | setContentsMargins(10, 10, 10, 10); 18 | } 19 | 20 | ProgressSlider* ProgressContainerLayout::addProgressBar(obs_source_t* source) 21 | { 22 | QHBoxLayout* mediaControlsLayout = new QHBoxLayout(); 23 | QStatusBar* statusBar = new QStatusBar(); 24 | 25 | ProgressSlider* progressBar = new ProgressSlider(); 26 | progressBar->setOrientation(Qt::Horizontal); 27 | progressBar->setStyleSheet("QSlider::groove:horizontal { height: 25 } QSlider::handle:horizontal { height: 25; width: 10 }"); 28 | 29 | progressBar->setTracking(false); 30 | 31 | QTimer* seekTimer = new QTimer(this); 32 | connect(seekTimer, &QTimer::timeout, [=]() 33 | { 34 | if (progressBar->previousState == OBS_MEDIA_STATE_ENDED) 35 | { 36 | obs_source_media_restart(source); 37 | obs_source_media_play_pause(source, true); 38 | progressBar->previousState = OBS_MEDIA_STATE_PLAYING; 39 | } 40 | 41 | if (progressBar->time != progressBar->lastTime) 42 | { 43 | obs_source_media_set_time(source, progressBar->time); 44 | qDebug() << "Timer: Media time being set to " << progressBar->time; 45 | } 46 | progressBar->lastTime = progressBar->time; 47 | }); 48 | 49 | connect(progressBar, &QSlider::sliderPressed, [=]() 50 | { 51 | progressBar->canChange = false; 52 | 53 | progressBar->previousState = obs_source_media_get_state(source); 54 | 55 | if (progressBar->previousState == OBS_MEDIA_STATE_PLAYING) 56 | { 57 | obs_source_media_play_pause(source, true); 58 | } 59 | 60 | progressBar->time = progressBar->value(); 61 | seekTimer->start(100); 62 | }); 63 | 64 | connect(progressBar, &QSlider::sliderReleased, [=]() 65 | { 66 | if (seekTimer->isActive()) 67 | { 68 | seekTimer->stop(); 69 | if (progressBar->time != progressBar->lastTime) 70 | { 71 | if (progressBar->previousState == OBS_MEDIA_STATE_ENDED) 72 | { 73 | obs_source_media_restart(source); 74 | } 75 | 76 | obs_source_media_set_time(source, progressBar->time); 77 | qDebug() << "Released: Media time being set to " << progressBar->time; 78 | } 79 | progressBar->time = progressBar->lastTime = -1; 80 | } 81 | 82 | if (progressBar->previousState != OBS_MEDIA_STATE_PAUSED) 83 | { 84 | obs_source_media_play_pause(source, false); 85 | } 86 | 87 | progressBar->canChange = true; 88 | }); 89 | 90 | connect(progressBar, &QSlider::sliderMoved, [=](int newSliderValue) 91 | { 92 | if (seekTimer->isActive()) 93 | { 94 | progressBar->time = newSliderValue; 95 | qDebug() << "Time variable is set to " << progressBar->time; 96 | } 97 | }); 98 | 99 | statusBar->addPermanentWidget(progressBar, 1); 100 | 101 | QLabel* label = new QLabel(); 102 | insertWidget(count() - 1, label); // Insert above the last stretch 103 | 104 | QWidget* mediaControls = new QWidget(); 105 | mediaControlsLayout->setContentsMargins(0, 0, 0, 10); 106 | mediaControls->setLayout(mediaControlsLayout); 107 | insertWidget(count() - 1, mediaControls); 108 | 109 | QPushButton* playPauseButton = addPlayPauseButton(mediaControlsLayout, source); 110 | addStopButton(mediaControlsLayout, source); 111 | 112 | mediaControlsLayout->addWidget(statusBar); 113 | 114 | QPushButton* loopToggleButton = addLoopToggleButton(mediaControlsLayout, source); 115 | 116 | videoWidgets[progressBar] = { label, playPauseButton, loopToggleButton }; 117 | 118 | return progressBar; 119 | } 120 | 121 | QWidget* ProgressContainerLayout::addSlideshow(obs_source_t* source) 122 | { 123 | const char* name = obs_source_get_name(source); 124 | 125 | QLabel* label = new QLabel(); 126 | insertWidget(count() - 1, label); // Insert above the last stretch 127 | 128 | QHBoxLayout* mediaControlsLayout = new QHBoxLayout(); 129 | addRestartButton(mediaControlsLayout, source); 130 | addPreviousButton(mediaControlsLayout, source); 131 | addNextButton(mediaControlsLayout, source); 132 | mediaControlsLayout->addStretch(); 133 | 134 | QWidget* mediaControls = new QWidget(); 135 | mediaControlsLayout->setContentsMargins(0, 0, 0, 10); 136 | mediaControls->setLayout(mediaControlsLayout); 137 | insertWidget(count() - 1, mediaControls); 138 | 139 | slideshowWidgets[mediaControls] = { label }; 140 | 141 | return mediaControls; 142 | } 143 | 144 | QPushButton* ProgressContainerLayout::addRestartButton(QHBoxLayout* layout, obs_source_t* source) 145 | { 146 | QPushButton* restartButton = new QPushButton(); 147 | restartButton->setMaximumWidth(25); 148 | restartButton->setIcon(Globals::restartIcon); 149 | restartButton->setToolTip("First slide"); 150 | 151 | connect(restartButton, &QPushButton::clicked, [=]() 152 | { 153 | obs_source_media_restart(source); 154 | }); 155 | 156 | layout->addWidget(restartButton); 157 | return restartButton; 158 | } 159 | 160 | QPushButton* ProgressContainerLayout::addPreviousButton(QHBoxLayout* layout, obs_source_t* source) 161 | { 162 | QPushButton* previousButton = new QPushButton(); 163 | previousButton->setMaximumWidth(25); 164 | previousButton->setIcon(Globals::previousIcon); 165 | previousButton->setToolTip("Previous slide"); 166 | 167 | connect(previousButton, &QPushButton::clicked, [=]() 168 | { 169 | obs_source_media_previous(source); 170 | }); 171 | 172 | layout->addWidget(previousButton); 173 | return previousButton; 174 | } 175 | 176 | QPushButton* ProgressContainerLayout::addNextButton(QHBoxLayout* layout, obs_source_t* source) 177 | { 178 | QPushButton* nextButton = new QPushButton(); 179 | nextButton->setMaximumWidth(25); 180 | nextButton->setIcon(Globals::nextIcon); 181 | nextButton->setToolTip("Next slide"); 182 | 183 | connect(nextButton, &QPushButton::clicked, [=]() 184 | { 185 | obs_source_media_next(source); 186 | }); 187 | 188 | layout->addWidget(nextButton); 189 | return nextButton; 190 | } 191 | 192 | QPushButton* ProgressContainerLayout::addPlayPauseButton(QHBoxLayout* layout, obs_source_t* source) 193 | { 194 | QPushButton* playPauseButton = new QPushButton(); 195 | playPauseButton->setObjectName("PlayPauseButton"); 196 | playPauseButton->setIcon(Globals::pauseIcon); 197 | playPauseButton->setMaximumWidth(25); 198 | 199 | // Connect the clicked event on the button to the signal mapper 200 | connect(playPauseButton, &QPushButton::clicked, [=]() 201 | { 202 | obs_media_state playState = obs_source_media_get_state(source); 203 | 204 | bool nowPlaying; 205 | if (playState == OBS_MEDIA_STATE_ENDED) 206 | { 207 | obs_source_media_restart(source); 208 | nowPlaying = true; 209 | } 210 | else 211 | { 212 | obs_source_media_play_pause(source, playState == OBS_MEDIA_STATE_PLAYING); 213 | 214 | // If it was playing before we sent the command to toggle state, then it is not playing now. 215 | // Hence why nowPlaying is true if the previous playing state was not. 216 | nowPlaying = playState != OBS_MEDIA_STATE_PLAYING; 217 | } 218 | 219 | // Update the button text 220 | playPauseButton->setIcon(nowPlaying ? Globals::pauseIcon : Globals::playIcon); 221 | }); 222 | 223 | layout->addWidget(playPauseButton); 224 | return playPauseButton; 225 | } 226 | 227 | QPushButton* ProgressContainerLayout::addStopButton(QHBoxLayout* layout, obs_source_t* source) 228 | { 229 | QPushButton* stopButton = new QPushButton(); 230 | stopButton->setIcon(Globals::stopIcon); 231 | stopButton->setMaximumWidth(25); 232 | 233 | connect(stopButton, &QPushButton::clicked, [=]() 234 | { 235 | obs_media_state playState = obs_source_media_get_state(source); 236 | if (playState != OBS_MEDIA_STATE_ENDED) 237 | { 238 | obs_source_media_stop(source); 239 | } 240 | }); 241 | 242 | layout->addWidget(stopButton); 243 | return stopButton; 244 | } 245 | 246 | QPushButton* ProgressContainerLayout::addLoopToggleButton(QHBoxLayout* layout, obs_source_t* source) 247 | { 248 | QPushButton* loopToggleButton = new QPushButton(); 249 | loopToggleButton->setObjectName("LoopToggleButton"); 250 | loopToggleButton->setIcon(Globals::loopIcon); 251 | loopToggleButton->setToolTip("Toggle loop"); 252 | loopToggleButton->setCheckable(true); 253 | loopToggleButton->setMaximumWidth(25); 254 | 255 | obs_data_t* initialSettings = obs_source_get_settings(source); // Must release 256 | const bool initialLoop = obs_data_get_bool(initialSettings, "looping"); 257 | obs_data_release(initialSettings); 258 | 259 | loopToggleButton->setChecked(initialLoop); 260 | 261 | connect(loopToggleButton, &QPushButton::clicked, [=]() 262 | { 263 | obs_data_t* settings = obs_source_get_settings(source); 264 | obs_data_set_bool(settings, "looping", loopToggleButton->isChecked()); 265 | 266 | // Important: Must persist the settings back to the source 267 | obs_source_update(source, settings); 268 | 269 | obs_data_release(settings); 270 | }); 271 | 272 | layout->addWidget(loopToggleButton); 273 | return loopToggleButton; 274 | } 275 | 276 | std::vector ProgressContainerLayout::getWidget(ProgressSlider* progressBar) 277 | { 278 | if (videoWidgets.contains(progressBar)) 279 | { 280 | return videoWidgets[progressBar]; 281 | } 282 | else 283 | { 284 | return { }; 285 | } 286 | } 287 | 288 | QLabel* ProgressContainerLayout::getLabel(ProgressSlider* progressBar) 289 | { 290 | if (videoWidgets.contains(progressBar)) 291 | { 292 | for (auto& widget : videoWidgets[progressBar]) 293 | { 294 | if (typeid(*widget) == typeid(QLabel)) 295 | { 296 | return dynamic_cast(widget); 297 | } 298 | } 299 | } 300 | 301 | return new QLabel(); 302 | } 303 | 304 | QLabel* ProgressContainerLayout::getLabel(QWidget* mediaControls) 305 | { 306 | if (slideshowWidgets.contains(mediaControls)) 307 | { 308 | for (auto& widget : slideshowWidgets[mediaControls]) 309 | { 310 | if (typeid(*widget) == typeid(QLabel)) 311 | { 312 | return dynamic_cast(widget); 313 | } 314 | } 315 | } 316 | 317 | return new QLabel(); 318 | } 319 | 320 | QPushButton* ProgressContainerLayout::getPlayPauseButton(ProgressSlider* progressBar) 321 | { 322 | if (videoWidgets.contains(progressBar)) 323 | { 324 | for (auto& widget : videoWidgets[progressBar]) 325 | { 326 | if (typeid(*widget) == typeid(QPushButton)) 327 | { 328 | QPushButton* button = dynamic_cast(widget); 329 | if (button->objectName() == "PlayPauseButton") 330 | { 331 | return button; 332 | } 333 | } 334 | } 335 | } 336 | 337 | return new QPushButton(); 338 | } 339 | 340 | QPushButton* ProgressContainerLayout::getLoopToggleButton(ProgressSlider* progressBar) 341 | { 342 | if (videoWidgets.contains(progressBar)) 343 | { 344 | for (auto& widget : videoWidgets[progressBar]) 345 | { 346 | if (typeid(*widget) == typeid(QPushButton)) 347 | { 348 | QPushButton* button = dynamic_cast(widget); 349 | if (button->objectName() == "LoopToggleButton") 350 | { 351 | return button; 352 | } 353 | } 354 | } 355 | } 356 | 357 | return new QPushButton(); 358 | } 359 | 360 | 361 | ProgressContainerLayout::~ProgressContainerLayout() 362 | { 363 | 364 | } -------------------------------------------------------------------------------- /src/ProgressContainerLayout.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "ProgressSlider.h" 9 | 10 | class ProgressContainerLayout : public QVBoxLayout 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit ProgressContainerLayout(QWidget* = 0); 16 | ~ProgressContainerLayout(); 17 | ProgressSlider* addProgressBar(obs_source_t*); 18 | QWidget* addSlideshow(obs_source_t*); 19 | std::vector getWidget(ProgressSlider*); 20 | QLabel* getLabel(ProgressSlider*); 21 | QLabel* getLabel(QWidget*); 22 | QPushButton* getPlayPauseButton(ProgressSlider*); 23 | QPushButton* getLoopToggleButton(ProgressSlider*); 24 | 25 | private: 26 | QMap> videoWidgets; 27 | QMap> slideshowWidgets; 28 | QPushButton* addPlayPauseButton(QHBoxLayout*, obs_source_t*); 29 | QPushButton* addStopButton(QHBoxLayout*, obs_source_t*); 30 | QPushButton* addLoopToggleButton(QHBoxLayout*, obs_source_t*); 31 | QPushButton* addRestartButton(QHBoxLayout*, obs_source_t*); 32 | QPushButton* addPreviousButton(QHBoxLayout*, obs_source_t*); 33 | QPushButton* addNextButton(QHBoxLayout*, obs_source_t*); 34 | }; 35 | -------------------------------------------------------------------------------- /src/ProgressDockWidget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "ProgressDockWidget.h" 4 | 5 | ProgressDockWidget::ProgressDockWidget(QWidget* parent) 6 | : QDockWidget(parent) 7 | { 8 | setObjectName("ProgressDockWidget"); 9 | setWindowTitle("Media Progress"); 10 | setFloating(false); 11 | 12 | QScrollArea* scrollArea = new QScrollArea(this); 13 | 14 | container = new QWidget(); 15 | scrollArea->setWidget(container); 16 | scrollArea->setWidgetResizable(true); 17 | 18 | layout = new ProgressContainerLayout(); 19 | 20 | container->setLayout(layout); 21 | setWidget(scrollArea); 22 | } 23 | 24 | ProgressSlider* ProgressDockWidget::addProgress(obs_source_t* source) const 25 | { 26 | return layout->addProgressBar(source); 27 | } 28 | 29 | QWidget* ProgressDockWidget::addSlideshow(obs_source_t* source) const 30 | { 31 | return layout->addSlideshow(source); 32 | } 33 | 34 | void ProgressDockWidget::clearProgressBars() const 35 | { 36 | for (QWidget* widget : container->findChildren(QString{}, Qt::FindDirectChildrenOnly)) 37 | { 38 | delete widget; 39 | } 40 | } 41 | 42 | ProgressDockWidget::~ProgressDockWidget() 43 | { 44 | //delete statusBar; 45 | } 46 | -------------------------------------------------------------------------------- /src/ProgressDockWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "ProgressContainerLayout.h" 6 | 7 | class ProgressDockWidget : public QDockWidget 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | explicit ProgressDockWidget(QWidget* parent = 0); 13 | ~ProgressDockWidget(); 14 | ProgressSlider* addProgress(obs_source_t*) const; 15 | QWidget* addSlideshow(obs_source_t*) const; 16 | void clearProgressBars() const; 17 | 18 | ProgressContainerLayout* layout; 19 | 20 | private: 21 | QWidget* container; 22 | }; 23 | -------------------------------------------------------------------------------- /src/ProgressSlider.cpp: -------------------------------------------------------------------------------- 1 | #include "ProgressSlider.h" 2 | #include "SliderStyle.h" 3 | 4 | ProgressSlider::ProgressSlider(QWidget* parent) 5 | : QSlider(parent), canChange(true), lastTime(-1), time(-1), previousState(OBS_MEDIA_STATE_NONE) 6 | { 7 | setStyle(new SliderStyle()); 8 | } -------------------------------------------------------------------------------- /src/ProgressSlider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ProgressSlider : public QSlider 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | explicit ProgressSlider(QWidget* parent = 0); 12 | bool canChange; 13 | int64_t lastTime; 14 | int64_t time; 15 | obs_media_state previousState; 16 | }; 17 | -------------------------------------------------------------------------------- /src/SliderStyle.cpp: -------------------------------------------------------------------------------- 1 | // From: https://github.com/obsproject/obs-studio/blob/0ba9b201a78b022b863db5aba84a6c34c348249d/UI/slider-absoluteset-style.cpp 2 | 3 | #include "SliderStyle.h" 4 | 5 | SliderStyle::SliderStyle(const QString& baseStyle) 6 | : QProxyStyle(baseStyle) 7 | { 8 | } 9 | SliderStyle::SliderStyle(QStyle* baseStyle) 10 | : QProxyStyle(baseStyle) 11 | { 12 | } 13 | 14 | int SliderStyle::styleHint(QStyle::StyleHint hint, 15 | const QStyleOption* option = 0, 16 | const QWidget* widget = 0, 17 | QStyleHintReturn* returnData = 0) const 18 | { 19 | if (hint == QStyle::SH_Slider_AbsoluteSetButtons) 20 | return (Qt::LeftButton | Qt::MiddleButton); 21 | return QProxyStyle::styleHint(hint, option, widget, returnData); 22 | } 23 | -------------------------------------------------------------------------------- /src/SliderStyle.h: -------------------------------------------------------------------------------- 1 | // From: https://github.com/obsproject/obs-studio/blob/0ba9b201a78b022b863db5aba84a6c34c348249d/UI/slider-absoluteset-style.hpp 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | class SliderStyle : public QProxyStyle { 8 | public: 9 | SliderStyle(const QString& baseStyle); 10 | SliderStyle(QStyle* baseStyle = Q_NULLPTR); 11 | int styleHint(QStyle::StyleHint hint, const QStyleOption* option, 12 | const QWidget* widget, 13 | QStyleHintReturn* returnData) const; 14 | }; -------------------------------------------------------------------------------- /src/obs-progress.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "ProgressDockWidget.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "Globals.h" 14 | #include "ProgressSlider.h" 15 | 16 | using namespace std::chrono; 17 | 18 | OBS_DECLARE_MODULE() 19 | OBS_MODULE_USE_DEFAULT_LOCALE("obs-progress", "en-US") 20 | 21 | ProgressDockWidget* _progressDockWidget; 22 | std::map media_states; 23 | void startTimer(); 24 | void stopTimer(); 25 | void timerHit(); 26 | void updateSceneInfo(); 27 | 28 | QTimer* _timer; 29 | QMap _videoSources; 30 | QMap _slideshowSources; 31 | 32 | QString progressBarTitleFormat = "'%1' Source %2 / %3 / -%4"; 33 | QString slideshowFormat = "'%1' Slideshow Source %2 / %3"; 34 | 35 | typedef bool (*scene_items_callback)(obs_scene_t*, obs_sceneitem_t*, void*); 36 | 37 | void obs_module_unload(void) 38 | { 39 | blog(LOG_INFO, "obs-progress: Unloading"); 40 | 41 | stopTimer(); 42 | } 43 | 44 | bool obs_module_load(void) 45 | { 46 | blog(LOG_INFO, "obs-progress: Loading"); 47 | 48 | Globals::initialize(); 49 | 50 | _videoSources = QMap(); 51 | _slideshowSources = QMap(); 52 | 53 | QMainWindow* mainWindow = static_cast(obs_frontend_get_main_window()); 54 | _progressDockWidget = new ProgressDockWidget(mainWindow); 55 | obs_frontend_add_dock(_progressDockWidget); 56 | 57 | startTimer(); 58 | updateSceneInfo(); 59 | 60 | // Setup event handler to start the server once OBS is ready 61 | auto eventCallback = [](enum obs_frontend_event event, void* param) { 62 | if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) { 63 | updateSceneInfo(); 64 | } 65 | else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) { 66 | blog(LOG_INFO, "obs-progress: Got scene collection cleanup event. Had %d video source(s), %d slideshow source(s). Clearing.", _videoSources.count(), _slideshowSources.count()); 67 | _videoSources.clear(); 68 | _slideshowSources.clear(); 69 | } 70 | }; 71 | obs_frontend_add_event_callback(eventCallback, static_cast(static_cast(eventCallback))); 72 | 73 | blog(LOG_INFO, "obs-progress: Successfully loaded"); 74 | 75 | return true; 76 | } 77 | 78 | void startTimer() 79 | { 80 | QMainWindow* mainWindow = static_cast(obs_frontend_get_main_window()); 81 | _timer = new QTimer(mainWindow); 82 | QObject::connect(_timer, &QTimer::timeout, timerHit); 83 | _timer->start(500); 84 | } 85 | 86 | void stopTimer() 87 | { 88 | try 89 | { 90 | //_timer->stop(); 91 | } 92 | catch (...) 93 | { 94 | // Empty 95 | } 96 | } 97 | 98 | void timerHit() 99 | { 100 | try 101 | { 102 | if (!_videoSources.isEmpty()) 103 | { 104 | QMapIterator i(_videoSources); 105 | while (i.hasNext()) 106 | { 107 | i.next(); 108 | 109 | auto* currentSceneItemSource = i.key(); 110 | 111 | // Get the times 112 | auto time = obs_source_media_get_time(currentSceneItemSource); 113 | auto duration = obs_source_media_get_duration(currentSceneItemSource); 114 | 115 | // Convert the times to a date format 116 | auto timeTimePoint = floor(time_point_cast(system_clock::time_point(duration_cast(milliseconds(time))))); 117 | auto durationTimePoint = floor(time_point_cast(system_clock::time_point(duration_cast(milliseconds(duration))))); 118 | auto remainingTimePoint = ceil(time_point_cast(system_clock::time_point(duration_cast(milliseconds(duration - time))))); 119 | QString progressBarText = progressBarTitleFormat.arg(obs_source_get_name(currentSceneItemSource), 120 | date::format("%T", timeTimePoint).c_str(), 121 | date::format("%T", durationTimePoint).c_str(), 122 | date::format("%T", remainingTimePoint).c_str() 123 | ); 124 | 125 | // Update the times on the label 126 | QLabel* labelToUpdate = _progressDockWidget->layout->getLabel(i.value()); 127 | labelToUpdate->setText(progressBarText); 128 | 129 | if (i.value()->canChange) 130 | { 131 | // Set the times on the progress bar 132 | i.value()->setRange(0, duration); 133 | i.value()->setValue(time); 134 | } 135 | 136 | // Get the state of the source and see if it's ended 137 | obs_media_state state = obs_source_media_get_state(currentSceneItemSource); 138 | if (state == OBS_MEDIA_STATE_ENDED || state == OBS_MEDIA_STATE_PAUSED) 139 | { 140 | QPushButton* playPauseButton = _progressDockWidget->layout->getPlayPauseButton(i.value()); 141 | playPauseButton->setIcon(Globals::playIcon); 142 | } 143 | else if (state == OBS_MEDIA_STATE_PLAYING) 144 | { 145 | // Sometimes the playback can start automatically (e.g., when settings are updated) 146 | // so we need to handle this case and update the play/pause button 147 | QPushButton* playPauseButton = _progressDockWidget->layout->getPlayPauseButton(i.value()); 148 | playPauseButton->setIcon(Globals::pauseIcon); 149 | } 150 | 151 | // Lastly, make sure the LoopToggle button is in the right state 152 | obs_data_t* settings = obs_source_get_settings(currentSceneItemSource); 153 | const bool loop = obs_data_get_bool(settings, "looping"); 154 | obs_data_release(settings); 155 | QPushButton* loopToggleButton = _progressDockWidget->layout->getLoopToggleButton(i.value()); 156 | loopToggleButton->setChecked(loop); 157 | } 158 | } 159 | 160 | if (!_slideshowSources.isEmpty()) 161 | { 162 | QMapIterator i(_slideshowSources); 163 | while (i.hasNext()) 164 | { 165 | i.next(); 166 | 167 | auto* currentSceneItemSource = i.key(); 168 | 169 | // From: https://github.com/obsproject/obs-studio/blob/bff7928b5069beea674274d42afab33528924dfd/UI/media-controls.cpp#L509-L525 170 | proc_handler_t* ph = obs_source_get_proc_handler(currentSceneItemSource); 171 | calldata_t cd = {}; 172 | 173 | proc_handler_call(ph, "current_index", &cd); 174 | int slide = calldata_int(&cd, "current_index"); 175 | 176 | proc_handler_call(ph, "total_files", &cd); 177 | int total = calldata_int(&cd, "total_files"); 178 | calldata_free(&cd); 179 | 180 | QString progressBarText; 181 | if (total < 0) 182 | { 183 | progressBarText = slideshowFormat.arg(obs_source_get_name(currentSceneItemSource), "-", "-"); 184 | } 185 | else 186 | { 187 | progressBarText = slideshowFormat.arg(obs_source_get_name(currentSceneItemSource), QString::number(slide + 1), QString::number(total)); 188 | } 189 | 190 | QLabel* labelToUpdate = _progressDockWidget->layout->getLabel(i.value()); 191 | labelToUpdate->setText(progressBarText); 192 | } 193 | } 194 | } 195 | catch (...) 196 | { 197 | // Empty 198 | } 199 | } 200 | 201 | void updateSceneInfo() 202 | { 203 | blog(LOG_INFO, "obs-progress: Got scene changed, rebuilding sources"); 204 | 205 | try 206 | { 207 | obs_source_t* currentSceneSource = obs_frontend_get_current_scene(); // This is the only call that increments the count, so we have to release it 208 | const char* name = obs_source_get_name(currentSceneSource); 209 | 210 | obs_scene_t* currentScene = obs_scene_from_source(currentSceneSource); 211 | obs_source_release(currentSceneSource); 212 | 213 | auto sceneItemsCallback = [](obs_scene_t* currentScene, obs_sceneitem_t* currentSceneItem, void* param) 214 | { 215 | obs_source_t* currentSceneItemSource = obs_sceneitem_get_source(currentSceneItem); 216 | const char* id = obs_source_get_unversioned_id(currentSceneItemSource); 217 | const bool isSlideshow = strcmp(id, "slideshow") == 0; 218 | 219 | // These are handy signals to potentially use in the future if we want to get notified of source changes. 220 | //signal_handler_t* handler = obs_source_get_signal_handler(currentSceneItemSource); 221 | //signal_handler_connect(handler, "destroy", source_destroyed, 0); 222 | 223 | //handler = obs_source_get_signal_handler(currentSceneItemSource); 224 | //signal_handler_connect(handler, "deactivate", source_deactivated, 0); 225 | 226 | if (obs_source_media_get_duration(currentSceneItemSource) > 0) 227 | { 228 | // Media source 229 | _videoSources[currentSceneItemSource] = _progressDockWidget->addProgress(currentSceneItemSource); 230 | } 231 | else if (isSlideshow) 232 | { 233 | // Slideshow source 234 | _slideshowSources[currentSceneItemSource] = _progressDockWidget->addSlideshow(currentSceneItemSource); 235 | } 236 | 237 | return true; 238 | }; 239 | 240 | _videoSources.clear(); 241 | _slideshowSources.clear(); 242 | _progressDockWidget->clearProgressBars(); 243 | obs_scene_enum_items(currentScene, sceneItemsCallback, static_cast(static_cast(sceneItemsCallback))); 244 | 245 | blog(LOG_INFO, "obs-progress: Tracking %d video source(s)", _videoSources.count()); 246 | blog(LOG_INFO, "obs-progress: Tracking %d slideshow source(s)", _slideshowSources.count()); 247 | 248 | _progressDockWidget->setWindowTitle("Progress of '" + QString(name) + "'"); 249 | } 250 | catch (...) 251 | { 252 | 253 | } 254 | } 255 | --------------------------------------------------------------------------------