├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── legacy.cmake ├── data └── locale │ ├── en-US.ini │ └── zh-CN.ini ├── mdkvideo.cpp ├── plugin.c └── screenshot └── obs-mdk-win32.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | mdk-sdk -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(obs-mdk) 3 | 4 | 5 | if(${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) 6 | find_package(libobs) 7 | if(NOT libobs_FOUND) 8 | find_package(LibObs) 9 | endif() 10 | endif() 11 | if(NOT TARGET OBS::libobs AND TARGET libobs) 12 | add_library(OBS::libobs ALIAS libobs) 13 | endif() 14 | 15 | set(MDKSDK "${CMAKE_CURRENT_LIST_DIR}/mdk-sdk" CACHE STRING "libmdk SDK dir") 16 | 17 | if(NOT CMAKE_PROJECT_NAME STREQUAL mdk) # not build in source tree 18 | list(APPEND CMAKE_MODULE_PATH ${MDKSDK}/lib/cmake) 19 | endif() 20 | find_package(MDK) 21 | 22 | add_library(${PROJECT_NAME} MODULE 23 | plugin.c 24 | mdkvideo.cpp 25 | ) 26 | add_library(OBS::mdk ALIAS ${PROJECT_NAME}) 27 | 28 | if(APPLE) 29 | target_compile_options(${PROJECT_NAME} PRIVATE -Wno-quoted-include-in-framework-header -Wno-newline-eof) 30 | endif() 31 | target_link_libraries(${PROJECT_NAME} 32 | OBS::libobs 33 | mdk 34 | ) 35 | 36 | if(EXISTS ${MDK_FRAMEWORK}) 37 | set_property(GLOBAL APPEND PROPERTY _OBS_FRAMEWORKS ${MDK_FRAMEWORK}) 38 | endif() 39 | 40 | if(NOT ${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) 41 | if(COMMAND legacy_check) 42 | legacy_check() 43 | endif() 44 | 45 | # cmake-format: off 46 | if(COMMAND set_target_properties_obs) 47 | set_target_properties_obs(${PROJECT_NAME} PROPERTIES FOLDER plugins/${PROJECT_NAME} PREFIX "") 48 | endif() 49 | # cmake-format: on 50 | 51 | add_custom_command( 52 | TARGET ${PROJECT_NAME} 53 | POST_BUILD 54 | COMMAND "${CMAKE_COMMAND}" -E echo "Add libmdk to destination" 55 | COMMAND 56 | "${CMAKE_COMMAND}" -E copy_if_different "${MDK_RUNTIME}" "${MDK_LIBASS}" 57 | "${OBS_OUTPUT_DIR}/$/${OBS_PLUGIN_DESTINATION}" 58 | COMMENT "" 59 | ) 60 | install( 61 | FILES 62 | "${MDK_RUNTIME}" 63 | "${MDK_LIBASS}" 64 | DESTINATION "${OBS_PLUGIN_DESTINATION}" 65 | COMPONENT Runtime 66 | ) 67 | elseif(EXISTS ${LIBOBS_PLUGIN_DESTINATION}) # linux 68 | install(TARGETS ${PROJECT_NAME} 69 | RUNTIME DESTINATION ${LIBOBS_PLUGIN_DESTINATION} 70 | LIBRARY DESTINATION ${LIBOBS_PLUGIN_DESTINATION} 71 | ) 72 | install( 73 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ 74 | DESTINATION ${LIBOBS_PLUGIN_DATA_DESTINATION}/${PROJECT_NAME} 75 | ) 76 | if(MDK_RUNTIMES) 77 | install( 78 | FILES ${MDK_RUNTIMES} # libmdk.so.0 can be loaded by MDK_PLUGINS from RUNPATH 79 | DESTINATION ${LIBOBS_PLUGIN_DESTINATION} 80 | COMPONENT ${PROJECT_NAME}_Runtime 81 | OPTIONAL 82 | ) 83 | endif() 84 | endif() 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 WangBin wbsecg1 at gmail dot com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Video Playback Plugin for OBS Studio. Based on [libmdk](https://github.com/wang-bin/mdk-sdk). [Download the latest prebuilt plugins for windows](https://sourceforge.net/projects/mdk-sdk/files/obs-plugin/) 2 | 3 | Features 4 | - Best performance: hardware decoding and rendering for all platforms 5 | - Playlist 6 | - Playback control via hotkey(Settings=>Hotkeys=>MDKVideo) 7 | - HDR, Dolby Vision display 8 | - Transparent videos: HEVC Alpha, VP8/9 Alpha, Hap 9 | 10 | NOTE: to use OpenGL renderer on windows, libEGL.dll and libGLESv2.dll in obs-plugins must be deleted 11 | 12 | Build: download [libmdk](https://sourceforge.net/projects/mdk-sdk/files/nightly/) and extract here 13 | 14 | 15 | Screen Shots 16 | 17 | ![obs-mdk-win32-dovi-alpha](https://github.com/user-attachments/assets/84351382-d8d8-4093-bdb2-7f92d5f56f5a) 18 | -------------------------------------------------------------------------------- /cmake/legacy.cmake: -------------------------------------------------------------------------------- 1 | 2 | set_target_properties(obs-mdk PROPERTIES FOLDER "plugins" PREFIX "") 3 | if(COMMAND setup_plugin_target) 4 | setup_plugin_target(obs-mdk) 5 | elseif(COMMAND install_obs_plugin_with_data) 6 | install_obs_plugin_with_data(obs-mdk data) 7 | endif() 8 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | LocalFile="Local File" 2 | Looping="Loop" 3 | Input="Input" 4 | SpeedPercentage="Speed" 5 | HWDecoder="Hardware Decoder" 6 | DecodeDevice="Decode Device" 7 | SameAsRenderer="Same as Renderer" -------------------------------------------------------------------------------- /data/locale/zh-CN.ini: -------------------------------------------------------------------------------- 1 | LocalFile="本地文件" 2 | Looping="循环" 3 | Input="输入" 4 | SpeedPercentage="速度" 5 | HWDecoder="硬件解码器" 6 | Playlist="播放列表" 7 | Auto="自动" 8 | DecodeDevice="解码设备" 9 | SameAsRenderer="同渲染器" -------------------------------------------------------------------------------- /mdkvideo.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 - 2025, WangBin wbsecg1 at gmail dot com and the obs-mdk contributors 3 | SPDX-License-Identifier: MIT 4 | */ 5 | #include 6 | #include 7 | #ifdef _WIN32 8 | #include 9 | #include 10 | #include 11 | using namespace Microsoft::WRL; //ComPtr 12 | #endif 13 | #include "mdk/Player.h" 14 | #if __has_include("mdk/AudioFrame.h") 15 | # define HAS_ON_AUDIO 1 16 | #endif 17 | using namespace MDK_NS; 18 | #include 19 | #include 20 | using namespace std; 21 | 22 | #define S_PLAYLIST "playlist" 23 | #define S_LOOP "loop" 24 | #define S_SHUFFLE "shuffle" 25 | 26 | #define T_(text) obs_module_text(text) 27 | #define T_PLAYLIST T_("Playlist") 28 | #define T_LOOP T_("LoopPlaylist") 29 | #define T_SHUFFLE T_("shuffle") 30 | 31 | #define EXTENSIONS_VIDEO \ 32 | "*.3gp *.3gpp *.asf *.avi;" \ 33 | "*.dv *.evo *.f4v *.flv;" \ 34 | "*.m2v *.m2t *.m2ts *.m4v *.mkv *.mov *.mp2 *.mp2v *.mp4;" \ 35 | "*.mp4v *.mpeg *.mpg *.mts;" \ 36 | "*.mtv *.mxf *.nsv *.nuv *.ogg *.ogm *.ogv;" \ 37 | "*.rm *.rmvb *.ts *.vob *.webm *.wm *.wmv" 38 | 39 | #define EXTENSIONS_PLAYLIST "*.cue *.m3u *.m3u8 *.pls;" 40 | 41 | #define EXTENSIONS_MEDIA \ 42 | EXTENSIONS_VIDEO " " EXTENSIONS_PLAYLIST 43 | 44 | #define MS_ENSURE(f, ...) MS_CHECK(f, return __VA_ARGS__;) 45 | #define MS_WARN(f) MS_CHECK(f) 46 | #define MS_CHECK(f, ...) do { \ 47 | while (FAILED(GetLastError())) {} \ 48 | HRESULT __ms_hr__ = f; \ 49 | if (FAILED(__ms_hr__)) { \ 50 | blog(LOG_WARNING, #f " ERROR@%d %s: (%#x) ", __LINE__, __FUNCTION__, __ms_hr__); \ 51 | __VA_ARGS__ \ 52 | } \ 53 | } while (false) 54 | 55 | constexpr int kMaxSpeedPercent = 400; 56 | 57 | auto from_obs(gs_color_space cs) { 58 | switch (cs) 59 | { 60 | case GS_CS_709_EXTENDED: 61 | return ColorSpaceExtendedLinearSRGB; 62 | case GS_CS_709_SCRGB: 63 | return ColorSpaceSCRGB; 64 | default: 65 | return ColorSpaceBT709; 66 | } 67 | } 68 | 69 | auto get_cs(const obs_video_info* ovi) { 70 | switch (ovi->colorspace) { 71 | case VIDEO_CS_2100_PQ: 72 | case VIDEO_CS_2100_HLG: 73 | return GS_CS_709_EXTENDED; 74 | default: 75 | return GS_CS_SRGB; 76 | } 77 | } 78 | 79 | static inline enum audio_format convert_sample_format(SampleFormat f) 80 | { 81 | switch (f) { 82 | case SampleFormat::U8: 83 | return AUDIO_FORMAT_U8BIT; 84 | case SampleFormat::S16: 85 | return AUDIO_FORMAT_16BIT; 86 | case SampleFormat::S32: 87 | return AUDIO_FORMAT_32BIT; 88 | case SampleFormat::F32: 89 | return AUDIO_FORMAT_FLOAT; 90 | case SampleFormat::U8P: 91 | return AUDIO_FORMAT_U8BIT_PLANAR; 92 | case SampleFormat::S16P: 93 | return AUDIO_FORMAT_16BIT_PLANAR; 94 | case SampleFormat::S32P: 95 | return AUDIO_FORMAT_32BIT_PLANAR; 96 | case SampleFormat::F32P: 97 | return AUDIO_FORMAT_FLOAT_PLANAR; 98 | default:; 99 | } 100 | 101 | return AUDIO_FORMAT_UNKNOWN; 102 | } 103 | 104 | static inline enum speaker_layout convert_speaker_layout(uint8_t channels) 105 | { 106 | switch (channels) { 107 | case 0: 108 | return SPEAKERS_UNKNOWN; 109 | case 1: 110 | return SPEAKERS_MONO; 111 | case 2: 112 | return SPEAKERS_STEREO; 113 | case 3: 114 | return SPEAKERS_2POINT1; 115 | case 4: 116 | return SPEAKERS_4POINT0; 117 | case 5: 118 | return SPEAKERS_4POINT1; 119 | case 6: 120 | return SPEAKERS_5POINT1; 121 | case 8: 122 | return SPEAKERS_7POINT1; 123 | default: 124 | return SPEAKERS_UNKNOWN; 125 | } 126 | } 127 | 128 | class mdkVideoSource { 129 | public: 130 | mdkVideoSource(obs_source_t* src) : source_(src) { 131 | next_it_ = urls_.cend(); 132 | setLogHandler([](LogLevel level, const char *msg) { 133 | int lv = LOG_DEBUG; 134 | switch (level) { 135 | case LogLevel::Info: 136 | lv = LOG_INFO; 137 | break; 138 | case LogLevel::Warning: 139 | lv = LOG_WARNING; 140 | break; 141 | case LogLevel::Error: 142 | lv = LOG_ERROR; 143 | break; 144 | default: 145 | break; 146 | } 147 | blog(lv, "%s", msg); 148 | }); 149 | player_.onMediaStatus([this](MediaStatus oldValue, MediaStatus newValue) { 150 | if (flags_added(oldValue, newValue, MediaStatus::Loaded)) { 151 | const auto codec = player_.mediaInfo().video[0].codec; 152 | w_ = codec.width; 153 | h_ = codec.height; 154 | obs_source_media_started(source_); 155 | } 156 | return true; 157 | }); 158 | player_.currentMediaChanged([this] { 159 | if (!player_.url()) 160 | return; 161 | if (next_it_ == urls_.cend()) { 162 | if (!loop_) 163 | return; 164 | next_it_ = urls_.cbegin(); 165 | } 166 | player_.setNextMedia(next_it_->data()); 167 | std::advance(next_it_, 1); 168 | }); 169 | 170 | player_.onStateChanged([this](State s) { 171 | if (s == State::Stopped) 172 | obs_source_media_stop(source_); 173 | }); 174 | 175 | #if (HAS_ON_AUDIO + 0) 176 | player_.setMute(true); 177 | player_.onFrame([this](AudioFrame &f, int track) { 178 | if (!f || f.timestamp() == TimestampEOS) 179 | return 0; 180 | struct obs_source_audio audio = {}; 181 | const auto planes = f.planeCount(); 182 | for (int i = 0; i < planes; i++) 183 | audio.data[i] = f.bufferData(i); 184 | 185 | audio.samples_per_sec = f.sampleRate(); 186 | audio.speakers = convert_speaker_layout(f.channels()); 187 | audio.format = convert_sample_format(f.format()); 188 | audio.frames = f.samplesPerChannel(); 189 | audio.timestamp = uint64_t(f.timestamp() * 1000000000); 190 | 191 | if (audio.format == AUDIO_FORMAT_UNKNOWN) 192 | return 0; 193 | obs_source_output_audio(source_, &audio); 194 | return 0; 195 | }); 196 | #endif // (HAS_ON_AUDIO + 0) 197 | play_pause_hotkey = obs_hotkey_register_source( 198 | source_, "MDKVideoSource.PlayPause", obs_module_text("PlayPause"), 199 | hotkeyPlayPause, this); 200 | 201 | restart_hotkey = obs_hotkey_register_source( 202 | source_, "MDKVideoSource.Restart", obs_module_text("Restart"), 203 | hotkeyRestart, this); 204 | 205 | stop_hotkey = obs_hotkey_register_source(source_, "MDKVideoSource.Stop", 206 | obs_module_text("Stop"), 207 | hotkeyStop, this); 208 | } 209 | 210 | ~mdkVideoSource() { 211 | setLogHandler(nullptr); // TODO: in module unload 212 | 213 | obs_enter_graphics(); 214 | gs_texrender_destroy(texrender_); 215 | obs_leave_graphics(); 216 | } 217 | 218 | gs_texture_t* render() { 219 | if (!ensureRTV()) 220 | return nullptr; 221 | player_.renderVideo(); 222 | gs_texrender_end(texrender_); 223 | return tex_; 224 | } 225 | 226 | void play(const char* url) { 227 | SetGlobalOption("sdr.white", obs_get_video_sdr_white_level()); 228 | player_.setNextMedia(nullptr); 229 | player_.set(State::Stopped); 230 | player_.waitFor(State::Stopped); 231 | player_.setMedia(nullptr); // 1st url may be the same as current url 232 | player_.setMedia(url); 233 | player_.set(State::Playing); 234 | } 235 | 236 | void restart() { 237 | player_.setMedia(nullptr); 238 | setUrls(urls_, loop_); 239 | } 240 | 241 | uint32_t width() const { return w_; } 242 | uint32_t height() const { return h_; } 243 | uint32_t flip() const { return flip_; } 244 | 245 | void setUrls(const list &urls, bool loop) 246 | { 247 | loop_ = loop; 248 | urls_ = urls; 249 | player_.setNextMedia(nullptr); 250 | if (urls_.empty()) { 251 | player_.set(State::Stopped); 252 | return; 253 | } 254 | string next; 255 | auto now = player_.url(); 256 | auto it = now ? find(urls_.cbegin(), urls_.cend(), now) : urls_.cend(); 257 | if (!now || it == urls_.cend()) { 258 | next_it_ = urls_.cbegin(); 259 | if (++next_it_ == urls_.cend() && loop_) 260 | next_it_ = urls_.cbegin(); 261 | play(urls_.front().data()); 262 | return; 263 | } 264 | next_it_ = ++it; 265 | if (it == urls_.cend()) { 266 | if (!loop_) { 267 | player_.setNextMedia(nullptr); 268 | return; 269 | } 270 | next_it_ = urls_.cbegin(); 271 | } 272 | player_.setNextMedia(next_it_->data()); 273 | } 274 | 275 | Player player_; 276 | private: 277 | #ifdef _WIN32 278 | ComPtr rtv_; 279 | #endif 280 | gs_color_space cs_ = GS_CS_SRGB; 281 | 282 | bool ensureRTV() { 283 | if (w_ <= 0 || h_ <= 0) 284 | return false; 285 | auto cs = gs_get_color_space(); // gs, not user settings 286 | obs_video_info ovi; 287 | if (obs_get_video_info(&ovi)) { // can be changed in settings dialog 288 | cs = get_cs(&ovi); 289 | } 290 | if (cs != cs_) 291 | player_.set(from_obs(cs)); 292 | const auto format = gs_get_format_from_space(cs); 293 | if (gs_texrender_get_format(texrender_) != format) { 294 | gs_texrender_destroy(texrender_); 295 | texrender_ = gs_texrender_create(format, GS_ZS_NONE); 296 | } 297 | gs_texrender_reset(texrender_); 298 | if (!gs_texrender_begin_with_color_space(texrender_, w_, h_, cs)) { 299 | blog(LOG_ERROR, "failed to begin texrender"); 300 | return false; 301 | } 302 | auto tex = gs_texrender_get_texture(texrender_); 303 | if (tex == tex_) 304 | return tex_; 305 | tex_ = tex; 306 | if (!tex_) 307 | return false; 308 | #ifdef _WIN32 309 | if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { 310 | flip_ = 0; 311 | D3D11RenderAPI ra{}; 312 | auto tex11 = (ID3D11Texture2D *)gs_texture_get_obj(tex_); 313 | ComPtr dev; 314 | tex11->GetDevice(&dev); 315 | D3D11_TEXTURE2D_DESC td; 316 | tex11->GetDesc(&td); 317 | D3D11_RENDER_TARGET_VIEW_DESC rtvd{}; 318 | rtvd.Format = td.Format; // rtvdesc can't be null since obs27(support srgb) because it's typeless format for GS_RGBA since 66259560 319 | if (td.Format == DXGI_FORMAT_R8G8B8A8_TYPELESS) { 320 | rtvd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; 321 | } 322 | rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; 323 | MS_ENSURE(dev->CreateRenderTargetView(tex11, &rtvd, &rtv_), false); 324 | ra.rtv = rtv_.Get(); 325 | player_.setRenderAPI(&ra); 326 | } 327 | #endif 328 | player_.setVideoSurfaceSize(w_, h_); 329 | player_.set(from_obs(cs)); 330 | return true; 331 | } 332 | 333 | 334 | static void hotkeyPlayPause(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) 335 | { 336 | auto c = static_cast(data); 337 | auto state = obs_source_media_get_state(c->source_); 338 | if (pressed && obs_source_active(c->source_)) { 339 | if (state == OBS_MEDIA_STATE_PLAYING) 340 | obs_source_media_play_pause(c->source_, true); 341 | else if (state == OBS_MEDIA_STATE_PAUSED) 342 | obs_source_media_play_pause(c->source_, false); 343 | } 344 | } 345 | 346 | static void hotkeyRestart(void *data, obs_hotkey_id, obs_hotkey_t*, bool pressed) 347 | { 348 | auto c = static_cast(data); 349 | if (pressed && obs_source_active(c->source_)) 350 | obs_source_media_restart(c->source_); 351 | } 352 | 353 | static void hotkeyStop(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) 354 | { 355 | auto c = static_cast(data); 356 | if (pressed && obs_source_active(c->source_)) 357 | obs_source_media_stop(c->source_); 358 | } 359 | 360 | bool loop_ = true; 361 | 362 | obs_source_t *source_ = nullptr; 363 | gs_texture_t *tex_ = nullptr; 364 | // required by opengl. d3d11 can simply use a texture as rtv, but opengl needs gl api calls here, which is not trival to support all cases because glx or egl used by obs is unknown(mdk does know that) 365 | gs_texrender_t* texrender_ = gs_texrender_create(GS_RGBA, GS_ZS_NONE); // rgb16f: pq, hlg trc, or 10bit source 366 | uint32_t flip_ = GS_FLIP_V; 367 | uint32_t w_ = 0; 368 | uint32_t h_ = 0; 369 | 370 | obs_hotkey_id play_pause_hotkey; 371 | obs_hotkey_id restart_hotkey; 372 | obs_hotkey_id stop_hotkey; 373 | obs_hotkey_id playlist_next_hotkey; 374 | obs_hotkey_id playlist_prev_hotkey; 375 | 376 | mutable list::const_iterator next_it_; 377 | list urls_; 378 | }; 379 | 380 | /* ------------------------------------------------------------------------- */ 381 | 382 | static const char* mdkvideo_getname(void*) 383 | { 384 | return "MDKVideo"; 385 | } 386 | 387 | static void mdkvideo_update(void* data, obs_data_t* settings) 388 | { 389 | auto obj = static_cast(data); 390 | //const char* url = obs_data_get_string(settings, "local_file"); 391 | bool loop = obs_data_get_bool(settings, "looping"); 392 | auto speed_percent = (int)obs_data_get_int(settings, "speed_percent"); 393 | if (speed_percent < 1 || speed_percent > kMaxSpeedPercent) 394 | speed_percent = 100; 395 | //obj->player_.setLoop(loop ? -1 : 0); 396 | obj->player_.setPlaybackRate(float(speed_percent) / 100.0f); 397 | 398 | auto adapter = obs_data_get_int(settings, "device"); 399 | if (adapter < 0) { 400 | obs_video_info ovi; 401 | if (obs_get_video_info(&ovi)) { 402 | adapter = ovi.adapter; 403 | } 404 | } 405 | 406 | string dec = obs_data_get_string(settings, "hwdecoder"); 407 | std::regex re(","); 408 | std::sregex_token_iterator first{dec.begin(), dec.end(), re, -1}, last; 409 | vector decs = { first, last }; 410 | if (adapter >= 0) { 411 | for (auto &dec : decs) { 412 | if (dec.find("MFT") == 0) { 413 | dec += ":adapter=" + to_string(adapter); 414 | } 415 | if (dec.find("D3D") == 0) { 416 | dec += ":hwdevice=" + to_string(adapter); 417 | } 418 | } 419 | } 420 | decs.insert(decs.end(), { "hap", "FFmpeg", "dav1d" }); 421 | obj->player_.setDecoders(MediaType::Video, decs); 422 | 423 | auto urls = obs_data_get_array(settings, S_PLAYLIST); 424 | auto nb_urls = obs_data_array_count(urls); 425 | list new_urls; 426 | for (size_t i = 0; i < nb_urls; i++) { 427 | obs_data_t *item = obs_data_array_item(urls, i); 428 | string p = obs_data_get_string(item, "value"); 429 | auto dir = os_opendir(p.data()); 430 | if (dir) { 431 | for (auto ent = os_readdir(dir); ent; ent = os_readdir(dir)) { 432 | if (ent->directory) 433 | continue; 434 | auto ext = os_get_path_extension(ent->d_name); 435 | if (strstr(EXTENSIONS_MEDIA, ext)) 436 | new_urls.push_back(p + "/" + ent->d_name); 437 | } 438 | os_closedir(dir); 439 | } else { 440 | new_urls.push_back(p); 441 | } 442 | obs_data_release(item); 443 | } 444 | obj->setUrls(new_urls, loop); 445 | } 446 | 447 | static void* mdkvideo_create(obs_data_t* settings, obs_source_t* source) 448 | { 449 | auto obj = new mdkVideoSource(source); 450 | mdkvideo_update(obj, settings); 451 | return obj; 452 | } 453 | 454 | static void mdkvideo_destroy(void* data) 455 | { 456 | auto obj = static_cast(data); 457 | delete obj; 458 | } 459 | 460 | static uint32_t mdkvideo_width(void* data) 461 | { 462 | auto obj = static_cast(data); 463 | return obj->width(); 464 | } 465 | 466 | static uint32_t mdkvideo_height(void* data) 467 | { 468 | auto obj = static_cast(data); 469 | return obj->height(); 470 | } 471 | 472 | static void mdkvideo_defaults(obs_data_t* settings) 473 | { 474 | obs_data_set_default_bool(settings, "looping", true); 475 | obs_data_set_default_int(settings, "speed_percent", 100); 476 | obs_data_set_default_int(settings, "device", -1); 477 | } 478 | 479 | static obs_properties_t* mdkvideo_properties(void*) 480 | { 481 | auto* props = obs_properties_create(); 482 | obs_property_t* p = nullptr; 483 | #if defined(_WIN32) 484 | p = obs_properties_add_list(props, "device", 485 | obs_module_text("DecodeDevice"), 486 | OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); 487 | obs_property_list_add_int(p, obs_module_text("SameAsRenderer"), -1); 488 | obs_enter_graphics(); 489 | 490 | obs_video_info ovi; 491 | if (obs_get_video_info(&ovi)) { 492 | } 493 | gs_enum_adapters( 494 | [](void *param, const char *name, uint32_t id) { 495 | auto p = (obs_property_t *)param; 496 | obs_property_list_add_int(p, name, 497 | id); 498 | return true; 499 | }, 500 | p); 501 | obs_leave_graphics(); 502 | #endif 503 | p = obs_properties_add_list(props, "hwdecoder", obs_module_text("HWDecoder"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); 504 | obs_property_list_add_string(p, obs_module_text("Auto"), 505 | #if defined(_WIN32) 506 | "MFT:d3d=11,D3D11,CUDA"); 507 | obs_property_list_add_string(p, "D3D11 via MFT", "MFT:d3d=11"); 508 | obs_property_list_add_string(p, "D3D12 via MFT", "MFT:d3d=12"); 509 | obs_property_list_add_string(p, "D3D11", "D3D11"); 510 | #elif defined(__APPLE__) 511 | "VT"); 512 | obs_property_list_add_string(p, "VT", "VT"); 513 | obs_property_list_add_string(p, "VideoToolbox", "VideoToolbox"); 514 | #else 515 | "VAAPI,VDPAU,CUDA"); 516 | obs_property_list_add_string(p, "VA-API", "VAAPI"); 517 | obs_property_list_add_string(p, "VDPAU", "VDPAU"); 518 | #endif 519 | #if !(__APPLE__+0) 520 | obs_property_list_add_string(p, "CUDA", "CUDA"); 521 | obs_property_list_add_string(p, "NVDEC", "NVDEC"); 522 | #endif 523 | obs_property_list_add_string(p, "None", "FFmpeg"); 524 | //obs_properties_add_path(props, "local_file", obs_module_text("LocalFile"), OBS_PATH_FILE, nullptr, nullptr); 525 | obs_properties_add_bool(props, "looping", obs_module_text("Looping")); 526 | auto prop = obs_properties_add_int_slider(props, "speed_percent", obs_module_text("SpeedPercentage"), 1, kMaxSpeedPercent, 1); 527 | obs_property_int_set_suffix(prop, "%"); 528 | 529 | auto filters = string("MediaFiles (") + EXTENSIONS_MEDIA + ")"; 530 | obs_properties_add_editable_list(props, S_PLAYLIST, T_PLAYLIST, 531 | OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS, 532 | filters.data(), nullptr); 533 | return props; 534 | } 535 | 536 | #if 0 537 | static void mdkvideo_tick(void* data, float /*seconds*/) 538 | { 539 | auto obj = static_cast(data); 540 | } 541 | #endif 542 | 543 | static void mdkvideo_render(void* data, gs_effect_t* effect) 544 | { 545 | auto obj = static_cast(data); 546 | auto tex = obj->render(); 547 | if (!tex) 548 | return; 549 | const bool linear_srgb = gs_get_linear_srgb(); 550 | const bool previous = gs_framebuffer_srgb_enabled(); 551 | gs_enable_framebuffer_srgb(linear_srgb); 552 | 553 | if (effect) { // if no OBS_SOURCE_CUSTOM_DRAW 554 | gs_eparam_t *image = gs_effect_get_param_by_name(effect, "image"); 555 | if (linear_srgb) { 556 | gs_effect_set_texture_srgb(image, tex); 557 | } else { 558 | gs_effect_set_texture(image, tex); 559 | } 560 | gs_draw_sprite(tex, obj->flip(), obj->width(), obj->height()); 561 | } else { 562 | effect = obs_get_base_effect(OBS_EFFECT_OPAQUE); 563 | gs_eparam_t *image =gs_effect_get_param_by_name(effect, "image"); 564 | if (linear_srgb) { 565 | gs_effect_set_texture_srgb(image, tex); 566 | } else { 567 | gs_effect_set_texture(image, tex); 568 | } 569 | while (gs_effect_loop(effect, "Draw")) 570 | gs_draw_sprite(tex, obj->flip(), 0, 0); 571 | } 572 | gs_enable_framebuffer_srgb(previous); 573 | } 574 | 575 | static void mdkvideo_play_pause(void *data, bool pause) 576 | { 577 | auto obj = static_cast(data); 578 | obj->player_.set(pause ? State::Paused : State::Playing); 579 | } 580 | 581 | static void mdkvideo_stop(void *data) 582 | { 583 | auto obj = static_cast(data); 584 | obj->player_.setNextMedia(nullptr); 585 | obj->player_.set(State::Stopped); 586 | } 587 | 588 | static void mdkvideo_restart(void *data) 589 | { 590 | SetGlobalOption("sdr.white", obs_get_video_sdr_white_level()); 591 | auto obj = static_cast(data); 592 | obj->restart(); 593 | } 594 | 595 | static int64_t mdkvideo_get_duration(void *data) 596 | { 597 | auto obj = static_cast(data); 598 | return obj->player_.mediaInfo().duration; 599 | } 600 | 601 | static int64_t mdkvideo_get_time(void *data) 602 | { 603 | auto obj = static_cast(data); 604 | return obj->player_.position(); 605 | } 606 | 607 | static void mdkvideo_set_time(void *data, int64_t ms) 608 | { 609 | auto obj = static_cast(data); 610 | obj->player_.seek(ms); 611 | } 612 | 613 | static enum obs_media_state mdkvideo_get_state(void *data) 614 | { 615 | auto obj = static_cast(data); 616 | auto s = obj->player_.mediaStatus(); 617 | if (test_flag(s & MediaStatus::Loading)) 618 | return OBS_MEDIA_STATE_OPENING; 619 | if (test_flag(s & MediaStatus::Buffering)) 620 | return OBS_MEDIA_STATE_BUFFERING; 621 | switch (obj->player_.state()) { 622 | case State::Playing: 623 | return OBS_MEDIA_STATE_PLAYING; 624 | case State::Paused: 625 | return OBS_MEDIA_STATE_PAUSED; 626 | case State::Stopped: 627 | return OBS_MEDIA_STATE_STOPPED; 628 | default: 629 | break; 630 | } 631 | return OBS_MEDIA_STATE_NONE; 632 | } 633 | 634 | static void mdkvideo_activate(void *data) 635 | { 636 | mdkvideo_play_pause(data, false); 637 | } 638 | 639 | static void mdkvideo_deactivate(void *data) 640 | { 641 | mdkvideo_play_pause(data, true); 642 | } 643 | 644 | enum gs_color_space 645 | mdkvideo_get_color_space(void *data, size_t count, 646 | const enum gs_color_space *preferred_spaces) 647 | { 648 | UNUSED_PARAMETER(data); 649 | UNUSED_PARAMETER(count); 650 | UNUSED_PARAMETER(preferred_spaces); 651 | 652 | enum gs_color_space space = GS_CS_SRGB; 653 | struct obs_video_info ovi; 654 | if (obs_get_video_info(&ovi)) { 655 | space = get_cs(&ovi); 656 | } 657 | 658 | return space; 659 | } 660 | 661 | extern "C" void register_mdkvideo() 662 | { 663 | static obs_source_info info; 664 | info.id = "mdkvideo"; 665 | info.type = OBS_SOURCE_TYPE_INPUT; 666 | info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CONTROLLABLE_MEDIA | OBS_SOURCE_DO_NOT_DUPLICATE 667 | | OBS_SOURCE_AUDIO 668 | | OBS_SOURCE_SRGB // for gs_get_linear_srgb() 669 | // | OBS_SOURCE_CUSTOM_DRAW 670 | ; 671 | info.get_name = mdkvideo_getname; 672 | info.create = mdkvideo_create; 673 | info.destroy = mdkvideo_destroy; 674 | info.update = mdkvideo_update; 675 | info.video_render = mdkvideo_render; 676 | //info.video_tick = mdkvideo_tick; 677 | info.activate = mdkvideo_activate; 678 | info.deactivate = mdkvideo_deactivate; 679 | info.get_width = mdkvideo_width; 680 | info.get_height = mdkvideo_height; 681 | info.get_defaults = mdkvideo_defaults; 682 | info.get_properties = mdkvideo_properties; 683 | info.icon_type = OBS_ICON_TYPE_MEDIA; 684 | info.media_play_pause = mdkvideo_play_pause; 685 | info.media_restart = mdkvideo_restart; 686 | info.media_stop = mdkvideo_stop; 687 | info.media_get_duration = mdkvideo_get_duration; 688 | info.media_get_time = mdkvideo_get_time; 689 | info.media_set_time = mdkvideo_set_time; 690 | info.media_get_state = mdkvideo_get_state; 691 | info.video_get_color_space = mdkvideo_get_color_space; 692 | obs_register_source(&info); 693 | } 694 | -------------------------------------------------------------------------------- /plugin.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 - 2023, WangBin wbsecg1 at gmail dot com and the obs-mdk contributors 3 | SPDX-License-Identifier: MIT 4 | */ 5 | #include 6 | 7 | OBS_DECLARE_MODULE() 8 | OBS_MODULE_USE_DEFAULT_LOCALE("mdk-video", "en-US") 9 | MODULE_EXPORT const char *obs_module_description(void) 10 | { 11 | return "mdk video source"; 12 | } 13 | 14 | extern void register_mdkvideo(); 15 | 16 | bool obs_module_load() 17 | { 18 | register_mdkvideo(); 19 | return true; 20 | } 21 | 22 | void obs_module_unload() 23 | {} 24 | -------------------------------------------------------------------------------- /screenshot/obs-mdk-win32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wang-bin/obs-mdk/d7ae12d25c0c0847d7f28b75d2ee8bd861a9266e/screenshot/obs-mdk-win32.jpg --------------------------------------------------------------------------------