diff --git a/cmake/Modules/FindLibVLC.cmake b/cmake/Modules/FindLibVLC.cmake new file mode 100644 index 000000000..3f6888555 --- /dev/null +++ b/cmake/Modules/FindLibVLC.cmake @@ -0,0 +1,72 @@ +# Once done these will be defined: +# +# LIBVLC_FOUND +# LIBVLC_INCLUDE_DIRS +# LIBVLC_LIBRARIES +# +# For use in OBS: +# +# VLC_INCLUDE_DIR + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_VLC QUIET VLC) +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +find_path(VLC_INCLUDE_DIR + NAMES libvlc.h + HINTS + ENV VLCPath${_lib_suffix} + ENV VLCPath + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${VLCPath${_lib_suffix}} + ${VLCPath} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_VLC_INCLUDE_DIRS} + PATHS + /usr/include /usr/local/include /opt/local/include /sw/include + PATH_SUFFIXES + vlc include/vlc include) + +find_library(VLC_LIB + NAMES ${_VLC_LIBRARIES} VLC libVLC + HINTS + ENV VLCPath${_lib_suffix} + ENV VLCPath + ENV DepsPath${_lib_suffix} + ENV DepsPath + ${VLCPath${_lib_suffix}} + ${VLCPath} + ${DepsPath${_lib_suffix}} + ${DepsPath} + ${_VLC_LIBRARY_DIRS} + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} lib + libs${_lib_suffix} libs + bin${_lib_suffix} bin + ../lib${_lib_suffix} ../lib + ../libs${_lib_suffix} ../libs + ../bin${_lib_suffix} ../bin) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibVLC_INCLUDES DEFAULT_MSG VLC_INCLUDE_DIR) +find_package_handle_standard_args(LibVLC DEFAULT_MSG VLC_LIB VLC_INCLUDE_DIR) +mark_as_advanced(VLC_INCLUDE_DIR VLC_LIB) + +if(LIBVLC_INCLUDES_FOUND) + set(LIBVLC_INCLUDE_DIRS ${VLC_INCLUDE_DIR}) +endif() + +if(LIBVLC_FOUND) + set(LIBVLC_LIBRARIES ${VLC_LIB}) +endif() diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 879118349..1ecb062b8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -10,6 +10,7 @@ if(WIN32) add_subdirectory(decklink/win) add_subdirectory(win-mf) add_subdirectory(obs-qsv11) + add_subdirectory(vlc-video) elseif(APPLE) add_subdirectory(coreaudio-encoder) add_subdirectory(mac-avcapture) @@ -17,6 +18,7 @@ elseif(APPLE) add_subdirectory(mac-vth264) add_subdirectory(mac-syphon) add_subdirectory(decklink/mac) + add_subdirectory(vlc-video) elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux") add_subdirectory(linux-capture) add_subdirectory(linux-pulseaudio) diff --git a/plugins/vlc-video/CMakeLists.txt b/plugins/vlc-video/CMakeLists.txt new file mode 100644 index 000000000..bcc95fdef --- /dev/null +++ b/plugins/vlc-video/CMakeLists.txt @@ -0,0 +1,41 @@ +project(vlc-video) + +if(DISABLE_VLC) + message(STATUS "VLC video plugin disabled") + return() +endif() + +find_package(LibVLC QUIET) + +if(NOT LIBVLC_INCLUDES_FOUND AND ENABLE_VLC) + message(FATAL_ERROR "LibVLC includes not found but set as enabled") +elseif(NOT LIBVLC_INCLUDES_FOUND) + message(STATUS "LibVLC includes not found, VLC video plugin disabled") + return() +endif() + +include_directories(${LIBVLC_INCLUDE_DIRS}) +add_definitions(${LIBVLC_DEFINITIONS}) + +if(MSVC) + set(vlc-video_PLATFORM_DEPS + w32-pthreads) +endif() + +set(vlc-video_HEADERS + vlc-video-plugin.h + ) + +set(vlc-video_SOURCES + vlc-video-plugin.c + vlc-video-source.c + ) + +add_library(vlc-video MODULE + ${vlc-video_SOURCES} + ${vlc-video_HEADERS}) +target_link_libraries(vlc-video + libobs + ${vlc-video_PLATFORM_DEPS}) + +install_obs_plugin_with_data(vlc-video data) diff --git a/plugins/vlc-video/data/locale/en-US.ini b/plugins/vlc-video/data/locale/en-US.ini new file mode 100644 index 000000000..cd5a7682a --- /dev/null +++ b/plugins/vlc-video/data/locale/en-US.ini @@ -0,0 +1,7 @@ +VLCSource="VLC Video Source" +Playlist="Playlist" +LoopPlaylist="Loop Playlist" +PlaybackBehavior="Visibility behavior" +PlaybackBehavior.StopRestart="Stop when not visible, restart when visible" +PlaybackBehavior.PauseUnpause="Pause when not visible, unpause when visible" +PlaybackBehavior.AlwaysPlay="Always play even when not visible" diff --git a/plugins/vlc-video/vlc-video-plugin.c b/plugins/vlc-video/vlc-video-plugin.c new file mode 100644 index 000000000..1f8e45bdb --- /dev/null +++ b/plugins/vlc-video/vlc-video-plugin.c @@ -0,0 +1,201 @@ +#ifdef _WIN32 +#include +#endif + +#include +#include "vlc-video-plugin.h" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("vlc-video", "en-US") + +/* libvlc core */ +LIBVLC_NEW libvlc_new_; +LIBVLC_RELEASE libvlc_release_; +LIBVLC_CLOCK libvlc_clock_; +LIBVLC_EVENT_ATTACH libvlc_event_attach_; + +/* libvlc media */ +LIBVLC_MEDIA_NEW_PATH libvlc_media_new_path_; +LIBVLC_MEDIA_RELEASE libvlc_media_release_; +LIBVLC_MEDIA_RELEASE libvlc_media_retain_; + +/* libvlc media player */ +LIBVLC_MEDIA_PLAYER_NEW libvlc_media_player_new_; +LIBVLC_MEDIA_PLAYER_NEW_FROM_MEDIA libvlc_media_player_new_from_media_; +LIBVLC_MEDIA_PLAYER_RELEASE libvlc_media_player_release_; +LIBVLC_VIDEO_SET_CALLBACKS libvlc_video_set_callbacks_; +LIBVLC_VIDEO_SET_FORMAT_CALLBACKS libvlc_video_set_format_callbacks_; +LIBVLC_AUDIO_SET_CALLBACKS libvlc_audio_set_callbacks_; +LIBVLC_AUDIO_SET_FORMAT_CALLBACKS libvlc_audio_set_format_callbacks_; +LIBVLC_MEDIA_PLAYER_PLAY libvlc_media_player_play_; +LIBVLC_MEDIA_PLAYER_STOP libvlc_media_player_stop_; +LIBVLC_MEDIA_PLAYER_GET_TIME libvlc_media_player_get_time_; +LIBVLC_VIDEO_GET_SIZE libvlc_video_get_size_; +LIBVLC_MEDIA_PLAYER_EVENT_MANAGER libvlc_media_player_event_manager_; + +/* libvlc media list */ +LIBVLC_MEDIA_LIST_NEW libvlc_media_list_new_; +LIBVLC_MEDIA_LIST_RELEASE libvlc_media_list_release_; +LIBVLC_MEDIA_LIST_ADD_MEDIA libvlc_media_list_add_media_; +LIBVLC_MEDIA_LIST_LOCK libvlc_media_list_lock_; +LIBVLC_MEDIA_LIST_UNLOCK libvlc_media_list_unlock_; +LIBVLC_MEDIA_LIST_EVENT_MANAGER libvlc_media_list_event_manager_; + +/* libvlc media list player */ +LIBVLC_MEDIA_LIST_PLAYER_NEW libvlc_media_list_player_new_; +LIBVLC_MEDIA_LIST_PLAYER_RELEASE libvlc_media_list_player_release_; +LIBVLC_MEDIA_LIST_PLAYER_PLAY libvlc_media_list_player_play_; +LIBVLC_MEDIA_LIST_PLAYER_PAUSE libvlc_media_list_player_pause_; +LIBVLC_MEDIA_LIST_PLAYER_STOP libvlc_media_list_player_stop_; +LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_PLAYER libvlc_media_list_player_set_media_player_; +LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_LIST libvlc_media_list_player_set_media_list_; +LIBVLC_MEDIA_LIST_PLAYER_EVENT_MANAGER libvlc_media_list_player_event_manager_; +LIBVLC_MEDIA_LIST_PLAYER_SET_PLAYBACK_MODE libvlc_media_list_player_set_playback_mode_; + +void *libvlc_module = NULL; +libvlc_instance_t *libvlc = NULL; +uint64_t time_start = 0; + +static bool load_vlc_funcs(void) +{ +#define LOAD_VLC_FUNC(func) \ + do { \ + func ## _ = os_dlsym(libvlc_module, #func); \ + if (!func ## _) { \ + blog(LOG_WARNING, "Could not func VLC function %s, " \ + "VLC loading failed", #func); \ + return false; \ + } \ + } while (false) + + /* libvlc core */ + LOAD_VLC_FUNC(libvlc_new); + LOAD_VLC_FUNC(libvlc_release); + LOAD_VLC_FUNC(libvlc_clock); + LOAD_VLC_FUNC(libvlc_event_attach); + + /* libvlc media */ + LOAD_VLC_FUNC(libvlc_media_new_path); + LOAD_VLC_FUNC(libvlc_media_release); + LOAD_VLC_FUNC(libvlc_media_retain); + + /* libvlc media player */ + LOAD_VLC_FUNC(libvlc_media_player_new); + LOAD_VLC_FUNC(libvlc_media_player_new_from_media); + LOAD_VLC_FUNC(libvlc_media_player_release); + LOAD_VLC_FUNC(libvlc_video_set_callbacks); + LOAD_VLC_FUNC(libvlc_video_set_format_callbacks); + LOAD_VLC_FUNC(libvlc_audio_set_callbacks); + LOAD_VLC_FUNC(libvlc_audio_set_format_callbacks); + LOAD_VLC_FUNC(libvlc_media_player_play); + LOAD_VLC_FUNC(libvlc_media_player_stop); + LOAD_VLC_FUNC(libvlc_media_player_get_time); + LOAD_VLC_FUNC(libvlc_video_get_size); + LOAD_VLC_FUNC(libvlc_media_player_event_manager); + + /* libvlc media list */ + LOAD_VLC_FUNC(libvlc_media_list_new); + LOAD_VLC_FUNC(libvlc_media_list_release); + LOAD_VLC_FUNC(libvlc_media_list_add_media); + LOAD_VLC_FUNC(libvlc_media_list_lock); + LOAD_VLC_FUNC(libvlc_media_list_unlock); + LOAD_VLC_FUNC(libvlc_media_list_event_manager); + + /* libvlc media list player */ + LOAD_VLC_FUNC(libvlc_media_list_player_new); + LOAD_VLC_FUNC(libvlc_media_list_player_release); + LOAD_VLC_FUNC(libvlc_media_list_player_play); + LOAD_VLC_FUNC(libvlc_media_list_player_pause); + LOAD_VLC_FUNC(libvlc_media_list_player_stop); + LOAD_VLC_FUNC(libvlc_media_list_player_set_media_player); + LOAD_VLC_FUNC(libvlc_media_list_player_set_media_list); + LOAD_VLC_FUNC(libvlc_media_list_player_event_manager); + LOAD_VLC_FUNC(libvlc_media_list_player_set_playback_mode); + return true; +} + +static bool load_libvlc_module(void) +{ +#ifdef _WIN32 + char *path_utf8 = NULL; + wchar_t path[1024]; + LSTATUS status; + DWORD size; + HKEY key; + + memset(path, 0, 1024 * sizeof(wchar_t)); + + status = RegOpenKeyW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\VideoLAN\\VLC", + &key); + if (status != ERROR_SUCCESS) + return false; + + size = 1024; + status = RegQueryValueExW(key, L"InstallDir", NULL, NULL, + (LPBYTE)path, &size); + if (status == ERROR_SUCCESS) { + wcscat(path, L"\\libvlc.dll"); + os_wcs_to_utf8_ptr(path, 0, &path_utf8); + libvlc_module = os_dlopen(path_utf8); + bfree(path_utf8); + } + + RegCloseKey(key); +#else + +#ifdef __APPLE__ +#define LIBVLC_DIR "/Applications/VLC.app/Contents/MacOS/" +#define LIBVLC_FILE LIBVLC_DIR "lib/libvlc.5.dylib" + setenv("VLC_PLUGIN_PATH", LIBVLC_DIR "plugins", false); +#else +#define LIBVLC_FILE "libvlc.5.so" +#endif + libvlc_module = os_dlopen(LIBVLC_FILE); + +#endif + + return libvlc_module != NULL; +} + +extern struct obs_source_info vlc_source_info; + +bool load_libvlc(void) +{ + if (libvlc) + return true; + + libvlc = libvlc_new_(0, 0); + if (!libvlc) { + blog(LOG_INFO, "Couldn't create libvlc instance"); + return false; + } + + time_start = (uint64_t)libvlc_clock_() * 1000ULL; + return true; +} + +bool obs_module_load(void) +{ + if (!load_libvlc_module()) { + blog(LOG_INFO, "Couldn't find VLC installation, VLC video " + "source disabled"); + return true; + } + + if (!load_vlc_funcs()) + return true; + + blog(LOG_INFO, "VLC found, VLC video source enabled"); + + obs_register_source(&vlc_source_info); + return true; +} + +void obs_module_unload(void) +{ + if (libvlc) + libvlc_release_(libvlc); + if (libvlc_module) + os_dlclose(libvlc_module); +} diff --git a/plugins/vlc-video/vlc-video-plugin.h b/plugins/vlc-video/vlc-video-plugin.h new file mode 100644 index 000000000..d564480e2 --- /dev/null +++ b/plugins/vlc-video/vlc-video-plugin.h @@ -0,0 +1,156 @@ +#include +#include + +#ifdef _MSC_VER +#include +typedef SSIZE_T ssize_t; +#endif + +#include +#include +#include +#include +#include + +extern libvlc_instance_t *libvlc; +extern uint64_t time_start; + +extern bool load_libvlc(void); + +/* libvlc core */ +typedef libvlc_instance_t *(*LIBVLC_NEW)(int argc, const char *const *argv); +typedef void (*LIBVLC_RELEASE)(libvlc_instance_t *p_instance); +typedef int64_t (*LIBVLC_CLOCK)(void); +typedef int (*LIBVLC_EVENT_ATTACH)(libvlc_event_manager_t *p_event_manager, + libvlc_event_type_t i_event_type, + libvlc_callback_t f_callback, + void *user_data); + +/* libvlc media */ +typedef libvlc_media_t *(*LIBVLC_MEDIA_NEW_PATH)( + libvlc_instance_t *p_instance, const char *path); +typedef void (*LIBVLC_MEDIA_RETAIN)(libvlc_media_t *p_md); +typedef void (*LIBVLC_MEDIA_RELEASE)(libvlc_media_t *p_md); + +/* libvlc media player */ +typedef libvlc_media_player_t *(*LIBVLC_MEDIA_PLAYER_NEW)( + libvlc_instance_t *p_libvlc); +typedef libvlc_media_player_t *(*LIBVLC_MEDIA_PLAYER_NEW_FROM_MEDIA)( + libvlc_media_t *p_md); +typedef void (*LIBVLC_MEDIA_PLAYER_RELEASE)( + libvlc_media_player_t *p_mi); +typedef void (*LIBVLC_VIDEO_SET_CALLBACKS)( + libvlc_media_player_t *mp, + libvlc_video_lock_cb lock, + libvlc_video_unlock_cb unlock, + libvlc_video_display_cb display, + void *opaque); +typedef void (*LIBVLC_VIDEO_SET_FORMAT_CALLBACKS)( + libvlc_media_player_t *mp, + libvlc_video_format_cb setup, + libvlc_video_cleanup_cb cleanup); +typedef void (*LIBVLC_AUDIO_SET_CALLBACKS)( + libvlc_media_player_t *mp, + libvlc_audio_play_cb play, + libvlc_audio_pause_cb pause, + libvlc_audio_resume_cb resume, + libvlc_audio_flush_cb flush, + libvlc_audio_drain_cb drain, + void *opaque); +typedef void (*LIBVLC_AUDIO_SET_FORMAT_CALLBACKS)( + libvlc_media_player_t *mp, + libvlc_audio_setup_cb setup, + libvlc_audio_cleanup_cb cleanup); +typedef int (*LIBVLC_MEDIA_PLAYER_PLAY)( + libvlc_media_player_t *p_mi); +typedef void (*LIBVLC_MEDIA_PLAYER_STOP)( + libvlc_media_player_t *p_mi); +typedef libvlc_time_t (*LIBVLC_MEDIA_PLAYER_GET_TIME)( + libvlc_media_player_t *p_mi); +typedef int (*LIBVLC_VIDEO_GET_SIZE)( + libvlc_media_player_t *p_mi, + unsigned num, + unsigned *px, + unsigned *py); +typedef libvlc_event_manager_t *(*LIBVLC_MEDIA_PLAYER_EVENT_MANAGER)( + libvlc_media_player_t *p_mp); + +/* libvlc media list */ +typedef libvlc_media_list_t *(*LIBVLC_MEDIA_LIST_NEW)( + libvlc_instance_t *p_instance); +typedef void (*LIBVLC_MEDIA_LIST_RELEASE)(libvlc_media_list_t *p_ml); +typedef int (*LIBVLC_MEDIA_LIST_ADD_MEDIA)(libvlc_media_list_t *p_ml, + libvlc_media_t *p_md); +typedef void (*LIBVLC_MEDIA_LIST_LOCK)(libvlc_media_list_t *p_ml); +typedef void (*LIBVLC_MEDIA_LIST_UNLOCK)(libvlc_media_list_t *p_ml); +typedef libvlc_event_manager_t *(*LIBVLC_MEDIA_LIST_EVENT_MANAGER)( + libvlc_media_list_t *p_ml); + +/* libvlc media list player */ +typedef libvlc_media_list_player_t *(*LIBVLC_MEDIA_LIST_PLAYER_NEW)( + libvlc_instance_t * p_instance); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_RELEASE)( + libvlc_media_list_player_t *p_mlp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_PLAY)( + libvlc_media_list_player_t *p_mlp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_PAUSE)( + libvlc_media_list_player_t *p_mlp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_STOP)( + libvlc_media_list_player_t *p_mlp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_PLAYER)( + libvlc_media_list_player_t *p_mlp, + libvlc_media_player_t *p_mp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_LIST)( + libvlc_media_list_player_t *p_mlp, + libvlc_media_list_t *p_mlist); +typedef libvlc_event_manager_t *(*LIBVLC_MEDIA_LIST_PLAYER_EVENT_MANAGER)( + libvlc_media_list_player_t *p_mlp); +typedef void (*LIBVLC_MEDIA_LIST_PLAYER_SET_PLAYBACK_MODE)( + libvlc_media_list_player_t *p_mlp, + libvlc_playback_mode_t e_mode); + +/* -------------------------------------------------------------------- */ + +/* libvlc core */ +extern LIBVLC_NEW libvlc_new_; +extern LIBVLC_RELEASE libvlc_release_; +extern LIBVLC_CLOCK libvlc_clock_; +extern LIBVLC_EVENT_ATTACH libvlc_event_attach_; + +/* libvlc media */ +extern LIBVLC_MEDIA_NEW_PATH libvlc_media_new_path_; +extern LIBVLC_MEDIA_RELEASE libvlc_media_release_; +extern LIBVLC_MEDIA_RETAIN libvlc_media_retain_; + +/* libvlc media player */ +extern LIBVLC_MEDIA_PLAYER_NEW libvlc_media_player_new_; +extern LIBVLC_MEDIA_PLAYER_NEW_FROM_MEDIA libvlc_media_player_new_from_media_; +extern LIBVLC_MEDIA_PLAYER_RELEASE libvlc_media_player_release_; +extern LIBVLC_VIDEO_SET_CALLBACKS libvlc_video_set_callbacks_; +extern LIBVLC_VIDEO_SET_FORMAT_CALLBACKS libvlc_video_set_format_callbacks_; +extern LIBVLC_AUDIO_SET_CALLBACKS libvlc_audio_set_callbacks_; +extern LIBVLC_AUDIO_SET_FORMAT_CALLBACKS libvlc_audio_set_format_callbacks_; +extern LIBVLC_MEDIA_PLAYER_PLAY libvlc_media_player_play_; +extern LIBVLC_MEDIA_PLAYER_STOP libvlc_media_player_stop_; +extern LIBVLC_MEDIA_PLAYER_GET_TIME libvlc_media_player_get_time_; +extern LIBVLC_VIDEO_GET_SIZE libvlc_video_get_size_; +extern LIBVLC_MEDIA_PLAYER_EVENT_MANAGER libvlc_media_player_event_manager_; + +/* libvlc media list */ +extern LIBVLC_MEDIA_LIST_NEW libvlc_media_list_new_; +extern LIBVLC_MEDIA_LIST_RELEASE libvlc_media_list_release_; +extern LIBVLC_MEDIA_LIST_ADD_MEDIA libvlc_media_list_add_media_; +extern LIBVLC_MEDIA_LIST_LOCK libvlc_media_list_lock_; +extern LIBVLC_MEDIA_LIST_UNLOCK libvlc_media_list_unlock_; +extern LIBVLC_MEDIA_LIST_EVENT_MANAGER libvlc_media_list_event_manager_; + +/* libvlc media list player */ +extern LIBVLC_MEDIA_LIST_PLAYER_NEW libvlc_media_list_player_new_; +extern LIBVLC_MEDIA_LIST_PLAYER_RELEASE libvlc_media_list_player_release_; +extern LIBVLC_MEDIA_LIST_PLAYER_PLAY libvlc_media_list_player_play_; +extern LIBVLC_MEDIA_LIST_PLAYER_PAUSE libvlc_media_list_player_pause_; +extern LIBVLC_MEDIA_LIST_PLAYER_STOP libvlc_media_list_player_stop_; +extern LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_PLAYER libvlc_media_list_player_set_media_player_; +extern LIBVLC_MEDIA_LIST_PLAYER_SET_MEDIA_LIST libvlc_media_list_player_set_media_list_; +extern LIBVLC_MEDIA_LIST_PLAYER_EVENT_MANAGER libvlc_media_list_player_event_manager_; +extern LIBVLC_MEDIA_LIST_PLAYER_SET_PLAYBACK_MODE libvlc_media_list_player_set_playback_mode_; diff --git a/plugins/vlc-video/vlc-video-source.c b/plugins/vlc-video/vlc-video-source.c new file mode 100644 index 000000000..8be1ecaf4 --- /dev/null +++ b/plugins/vlc-video/vlc-video-source.c @@ -0,0 +1,625 @@ +#include "vlc-video-plugin.h" +#include +#include +#include + +#define do_log(level, format, ...) \ + blog(level, "[vlc_source: '%s'] " format, \ + obs_source_get_name(ss->source), ##__VA_ARGS__) + +#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) + +#define S_PLAYLIST "playlist" +#define S_LOOP "loop" +#define S_BEHAVIOR "playback_behavior" +#define S_BEHAVIOR_STOP_RESTART "stop_restart" +#define S_BEHAVIOR_PAUSE_UNPAUSE "pause_unpause" +#define S_BEHAVIOR_ALWAYS_PLAY "always_play" + +#define T_(text) obs_module_text(text) +#define T_PLAYLIST T_("Playlist") +#define T_LOOP T_("LoopPlaylist") +#define T_BEHAVIOR T_("PlaybackBehavior") +#define T_BEHAVIOR_STOP_RESTART T_("PlaybackBehavior.StopRestart") +#define T_BEHAVIOR_PAUSE_UNPAUSE T_("PlaybackBehavior.PauseUnpause") +#define T_BEHAVIOR_ALWAYS_PLAY T_("PlaybackBehavior.AlwaysPlay") + +/* ------------------------------------------------------------------------- */ + +struct media_file_data { + char *path; + libvlc_media_t *media; +}; + +enum behavior { + BEHAVIOR_STOP_RESTART, + BEHAVIOR_PAUSE_UNPAUSE, + BEHAVIOR_ALWAYS_PLAY, +}; + +struct vlc_source { + obs_source_t *source; + + libvlc_media_player_t *media_player; + libvlc_media_list_player_t *media_list_player; + + struct obs_source_frame frame; + struct obs_source_audio audio; + size_t audio_capacity; + + pthread_mutex_t mutex; + DARRAY(struct media_file_data) files; + enum behavior behavior; + bool loop; +}; + +static libvlc_media_t *get_media(struct darray *array, const char *path) +{ + DARRAY(struct media_file_data) files; + libvlc_media_t *media = NULL; + + files.da = *array; + + for (size_t i = 0; i < files.num; i++) { + const char *cur_path = files.array[i].path; + + if (strcmp(path, cur_path) == 0) { + media = files.array[i].media; + libvlc_media_retain_(media); + break; + } + } + + return media; +} + +static inline libvlc_media_t *create_media_from_file(const char *file) +{ + return libvlc_media_new_path_(libvlc, file); +} + +static void free_files(struct darray *array) +{ + DARRAY(struct media_file_data) files; + files.da = *array; + + for (size_t i = 0; i < files.num; i++) { + bfree(files.array[i].path); + libvlc_media_release_(files.array[i].media); + } + + da_free(files); +} + +static inline bool chroma_is(const char *chroma, const char *val) +{ + return *(uint32_t*)chroma == *(uint32_t*)val; +} + +static enum video_format convert_vlc_video_format(char *chroma, bool *full) +{ + *full = false; + +#define CHROMA_TEST(val, ret) \ + if (chroma_is(chroma, val)) return ret +#define CHROMA_CONV(val, new_val, ret) \ + do { \ + if (chroma_is(chroma, val)) { \ + *(uint32_t*)chroma = *(uint32_t*)new_val; \ + return ret; \ + } \ + } while (false) +#define CHROMA_CONV_FULL(val, new_val, ret) \ + do { \ + *full = true; \ + CHROMA_CONV(val, new_val, ret); \ + } while (false) + + CHROMA_TEST("RGBA", VIDEO_FORMAT_RGBA); + CHROMA_TEST("BGRA", VIDEO_FORMAT_BGRA); + + /* 4:2:0 formats */ + CHROMA_TEST("NV12", VIDEO_FORMAT_NV12); + CHROMA_TEST("I420", VIDEO_FORMAT_I420); + CHROMA_TEST("IYUV", VIDEO_FORMAT_I420); + CHROMA_CONV("NV21", "NV12", VIDEO_FORMAT_NV12); + CHROMA_CONV("I422", "NV12", VIDEO_FORMAT_NV12); + CHROMA_CONV("Y42B", "NV12", VIDEO_FORMAT_NV12); + CHROMA_CONV("YV12", "NV12", VIDEO_FORMAT_NV12); + CHROMA_CONV("yv12", "NV12", VIDEO_FORMAT_NV12); + + CHROMA_CONV_FULL("J420", "J420", VIDEO_FORMAT_I420); + + /* 4:2:2 formats */ + CHROMA_TEST("UYVY", VIDEO_FORMAT_UYVY); + CHROMA_TEST("UYNV", VIDEO_FORMAT_UYVY); + CHROMA_TEST("UYNY", VIDEO_FORMAT_UYVY); + CHROMA_TEST("Y422", VIDEO_FORMAT_UYVY); + CHROMA_TEST("HDYC", VIDEO_FORMAT_UYVY); + CHROMA_TEST("AVUI", VIDEO_FORMAT_UYVY); + CHROMA_TEST("uyv1", VIDEO_FORMAT_UYVY); + CHROMA_TEST("2vuy", VIDEO_FORMAT_UYVY); + CHROMA_TEST("2Vuy", VIDEO_FORMAT_UYVY); + CHROMA_TEST("2Vu1", VIDEO_FORMAT_UYVY); + + CHROMA_TEST("YUY2", VIDEO_FORMAT_YUY2); + CHROMA_TEST("YUYV", VIDEO_FORMAT_YUY2); + CHROMA_TEST("YUNV", VIDEO_FORMAT_YUY2); + CHROMA_TEST("V422", VIDEO_FORMAT_YUY2); + + CHROMA_TEST("YVYU", VIDEO_FORMAT_YVYU); + + CHROMA_CONV("v210", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("cyuv", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("CYUV", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("VYUY", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("NV16", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("NV61", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("I410", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("I422", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("Y42B", "UYVY", VIDEO_FORMAT_UYVY); + CHROMA_CONV("J422", "UYVY", VIDEO_FORMAT_UYVY); + + /* 4:4:4 formats */ + CHROMA_TEST("I444", VIDEO_FORMAT_I444); + CHROMA_CONV_FULL("J444", "J444", VIDEO_FORMAT_I444); + CHROMA_CONV("YUVA", "RGBA", VIDEO_FORMAT_RGBA); + + /* 4:4:0 formats */ + CHROMA_CONV("I440", "I444", VIDEO_FORMAT_I444); + CHROMA_CONV("J440", "I444", VIDEO_FORMAT_I444); + + /* 4:1:0 formats */ + CHROMA_CONV("YVU9", "NV12", VIDEO_FORMAT_UYVY); + CHROMA_CONV("I410", "NV12", VIDEO_FORMAT_UYVY); + + /* 4:1:1 formats */ + CHROMA_CONV("I411", "NV12", VIDEO_FORMAT_UYVY); + CHROMA_CONV("Y41B", "NV12", VIDEO_FORMAT_UYVY); + + /* greyscale formats */ + CHROMA_TEST("GREY", VIDEO_FORMAT_Y800); + CHROMA_TEST("Y800", VIDEO_FORMAT_Y800); + CHROMA_TEST("Y8 ", VIDEO_FORMAT_Y800); +#undef CHROMA_CONV_FULL +#undef CHROMA_CONV +#undef CHROMA_TEST + + *(uint32_t*)chroma = *(uint32_t*)"BGRA"; + return VIDEO_FORMAT_BGRA; +} + +static inline unsigned get_format_lines(enum video_format format, + unsigned height, size_t plane) +{ + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + return (plane == 0) ? height : height / 2; + case VIDEO_FORMAT_YVYU: + case VIDEO_FORMAT_YUY2: + case VIDEO_FORMAT_UYVY: + case VIDEO_FORMAT_I444: + case VIDEO_FORMAT_RGBA: + case VIDEO_FORMAT_BGRA: + case VIDEO_FORMAT_BGRX: + case VIDEO_FORMAT_Y800: + return height; + case VIDEO_FORMAT_NONE:; + } + + return 0; +} + +static enum audio_format convert_vlc_audio_format(char *format) +{ +#define AUDIO_TEST(val, ret) \ + if (chroma_is(format, val)) return ret +#define AUDIO_CONV(val, new_val, ret) \ + do { \ + if (chroma_is(format, val)) { \ + *(uint32_t*)format = *(uint32_t*)new_val; \ + return ret; \ + } \ + } while (false) + + AUDIO_TEST("S16N", AUDIO_FORMAT_16BIT); + AUDIO_TEST("S32N", AUDIO_FORMAT_32BIT); + AUDIO_TEST("FL32", AUDIO_FORMAT_FLOAT); + + AUDIO_CONV("U16N", "S16N", AUDIO_FORMAT_16BIT); + AUDIO_CONV("U32N", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("S24N", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("U24N", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("FL64", "FL32", AUDIO_FORMAT_FLOAT); + + AUDIO_CONV("S16I", "S16N", AUDIO_FORMAT_16BIT); + AUDIO_CONV("U16I", "S16N", AUDIO_FORMAT_16BIT); + AUDIO_CONV("S24I", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("U24I", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("S32I", "S32N", AUDIO_FORMAT_32BIT); + AUDIO_CONV("U32I", "S32N", AUDIO_FORMAT_32BIT); +#undef AUDIO_CONV +#undef AUDIO_TEST + + *(uint32_t*)format = *(uint32_t*)"FL32"; + return AUDIO_FORMAT_FLOAT; +} + +/* ------------------------------------------------------------------------- */ + +static const char *vlcs_get_name(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("VLCSource"); +} + +static void vlcs_destroy(void *data) +{ + struct vlc_source *c = data; + + if (c->media_list_player) { + libvlc_media_list_player_stop_(c->media_list_player); + libvlc_media_list_player_release_(c->media_list_player); + } + if (c->media_player) { + libvlc_media_player_release_(c->media_player); + } + + bfree((void*)c->audio.data[0]); + obs_source_frame_free(&c->frame); + + free_files(&c->files.da); + pthread_mutex_destroy(&c->mutex); + bfree(c); +} + +static void *vlcs_video_lock(void *data, void **planes) +{ + struct vlc_source *c = data; + for (size_t i = 0; i < MAX_AV_PLANES && c->frame.data[i] != NULL; i++) + planes[i] = c->frame.data[i]; + return NULL; +} + +static void vlcs_video_display(void *data, void *picture) +{ + struct vlc_source *c = data; + c->frame.timestamp = (uint64_t)libvlc_clock_() * 1000ULL - time_start; + obs_source_output_video(c->source, &c->frame); + + UNUSED_PARAMETER(picture); +} + +static unsigned vlcs_video_format(void **p_data, char *chroma, unsigned *width, + unsigned *height, unsigned *pitches, unsigned *lines) +{ + struct vlc_source *c = *p_data; + enum video_format new_format; + enum video_range_type range; + bool new_range; + size_t i = 0; + + new_format = convert_vlc_video_format(chroma, &new_range); + + libvlc_video_get_size_(c->media_player, 0, width, height); + + /* don't allocate a new frame if format/width/height hasn't changed */ + if (c->frame.format != new_format || + c->frame.width != *width || + c->frame.height != *height) { + obs_source_frame_free(&c->frame); + obs_source_frame_init(&c->frame, new_format, *width, *height); + + c->frame.format = new_format; + c->frame.full_range = new_range; + range = c->frame.full_range ? + VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + video_format_get_parameters(VIDEO_CS_DEFAULT, range, + c->frame.color_matrix, + c->frame.color_range_min, + c->frame.color_range_max); + } + + while (c->frame.data[i]) { + pitches[i] = (unsigned)c->frame.linesize[i]; + lines[i] = get_format_lines(c->frame.format, *height, i); + i++; + } + + return 1; +} + +static void vlcs_audio_play(void *data, const void *samples, unsigned count, + int64_t pts) +{ + struct vlc_source *c = data; + size_t size = get_audio_size(c->audio.format, c->audio.speakers, count); + + if (c->audio_capacity < count) { + c->audio.data[0] = brealloc((void*)c->audio.data[0], size); + c->audio_capacity = count; + } + + memcpy((void*)c->audio.data[0], samples, size); + c->audio.timestamp = (uint64_t)pts * 1000ULL - time_start; + c->audio.frames = count; + + obs_source_output_audio(c->source, &c->audio); +} + +static int vlcs_audio_setup(void **p_data, char *format, unsigned *rate, + unsigned *channels) +{ + struct vlc_source *c = *p_data; + enum audio_format new_audio_format; + + new_audio_format = convert_vlc_audio_format(format); + if (*channels == 7 || *channels > 8) + *channels = 2; + + /* don't free audio data if the data is the same format */ + if (c->audio.format == new_audio_format && + c->audio.samples_per_sec == *rate && + c->audio.speakers == (enum speaker_layout)*channels) + return 0; + + c->audio_capacity = 0; + bfree((void*)c->audio.data[0]); + + memset(&c->audio, 0, sizeof(c->audio)); + c->audio.speakers = (enum speaker_layout)*channels; + c->audio.samples_per_sec = *rate; + c->audio.format = new_audio_format; + return 0; +} + +static void vlcs_update(void *data, obs_data_t *settings) +{ + DARRAY(struct media_file_data) new_files; + DARRAY(struct media_file_data) old_files; + libvlc_media_list_t *media_list; + struct vlc_source *c = data; + obs_data_array_t *array; + const char *behavior; + size_t count; + + da_init(new_files); + da_init(old_files); + + array = obs_data_get_array(settings, S_PLAYLIST); + count = obs_data_array_count(array); + + c->loop = obs_data_get_bool(settings, S_LOOP); + + behavior = obs_data_get_string(settings, S_BEHAVIOR); + + if (astrcmpi(behavior, S_BEHAVIOR_PAUSE_UNPAUSE) == 0) { + c->behavior = BEHAVIOR_PAUSE_UNPAUSE; + } else if (astrcmpi(behavior, S_BEHAVIOR_ALWAYS_PLAY) == 0) { + c->behavior = BEHAVIOR_ALWAYS_PLAY; + } else { /* S_BEHAVIOR_STOP_RESTART */ + c->behavior = BEHAVIOR_STOP_RESTART; + } + + /* ------------------------------------- */ + /* create new list of sources */ + + for (size_t i = 0; i < count; i++) { + obs_data_t *item = obs_data_array_item(array, i); + const char *path = obs_data_get_string(item, "value"); + struct media_file_data data; + struct dstr new_path = {0}; + libvlc_media_t *new_media; + + dstr_copy(&new_path, path); +#ifdef _WIN32 + dstr_replace(&new_path, "/", "\\"); +#endif + path = new_path.array; + + new_media = get_media(&c->files.da, path); + + if (!new_media) + new_media = get_media(&new_files.da, path); + if (!new_media) + new_media = create_media_from_file(path); + + if (new_media) { + data.path = new_path.array; + data.media = new_media; + da_push_back(new_files, &data); + } else { + dstr_free(&new_path); + } + + obs_data_release(item); + } + + /* ------------------------------------- */ + /* update settings data */ + + libvlc_media_list_player_stop_(c->media_list_player); + + pthread_mutex_lock(&c->mutex); + old_files.da = c->files.da; + c->files.da = new_files.da; + pthread_mutex_unlock(&c->mutex); + + /* ------------------------------------- */ + /* clean up and restart playback */ + + free_files(&old_files.da); + + media_list = libvlc_media_list_new_(libvlc); + + libvlc_media_list_lock_(media_list); + for (size_t i = 0; i < c->files.num; i++) + libvlc_media_list_add_media_(media_list, + c->files.array[i].media); + libvlc_media_list_unlock_(media_list); + + libvlc_media_list_player_set_media_list_(c->media_list_player, + media_list); + libvlc_media_list_release_(media_list); + + libvlc_media_list_player_set_playback_mode_(c->media_list_player, + c->loop ? libvlc_playback_mode_loop : + libvlc_playback_mode_default); + + if (c->files.num && + (c->behavior == BEHAVIOR_ALWAYS_PLAY || obs_source_active(c->source))) + libvlc_media_list_player_play_(c->media_list_player); + else + obs_source_output_video(c->source, NULL); + + obs_data_array_release(array); +} + +static void vlcs_stopped(const struct libvlc_event_t *event, void *data) +{ + struct vlc_source *c = data; + if (!c->loop) + obs_source_output_video(c->source, NULL); + + UNUSED_PARAMETER(event); +} + +static void *vlcs_create(obs_data_t *settings, obs_source_t *source) +{ + struct vlc_source *c = bzalloc(sizeof(*c)); + c->source = source; + + pthread_mutex_init_value(&c->mutex); + if (pthread_mutex_init(&c->mutex, NULL) != 0) + goto error; + + if (!load_libvlc()) + goto error; + + c->media_list_player = libvlc_media_list_player_new_(libvlc); + if (!c->media_list_player) + goto error; + + c->media_player = libvlc_media_player_new_(libvlc); + if (!c->media_player) + goto error; + + libvlc_media_list_player_set_media_player_(c->media_list_player, + c->media_player); + + libvlc_video_set_callbacks_(c->media_player, + vlcs_video_lock, NULL, vlcs_video_display, + c); + libvlc_video_set_format_callbacks_(c->media_player, + vlcs_video_format, NULL); + + libvlc_audio_set_callbacks_(c->media_player, + vlcs_audio_play, NULL, NULL, NULL, NULL, c); + libvlc_audio_set_format_callbacks_(c->media_player, + vlcs_audio_setup, NULL); + + libvlc_event_manager_t *event_manager; + event_manager = libvlc_media_player_event_manager_(c->media_player); + libvlc_event_attach_(event_manager, libvlc_MediaPlayerEndReached, + vlcs_stopped, c); + + obs_source_update(source, NULL); + + UNUSED_PARAMETER(settings); + return c; + +error: + vlcs_destroy(c); + return NULL; +} + +static void vlcs_activate(void *data) +{ + struct vlc_source *c = data; + + if (c->behavior == BEHAVIOR_STOP_RESTART) { + libvlc_media_list_player_play_(c->media_list_player); + + } else if (c->behavior == BEHAVIOR_PAUSE_UNPAUSE) { + libvlc_media_list_player_play_(c->media_list_player); + } +} + +static void vlcs_deactivate(void *data) +{ + struct vlc_source *c = data; + + if (c->behavior == BEHAVIOR_STOP_RESTART) { + libvlc_media_list_player_stop_(c->media_list_player); + obs_source_output_video(c->source, NULL); + + } else if (c->behavior == BEHAVIOR_PAUSE_UNPAUSE) { + libvlc_media_list_player_pause_(c->media_list_player); + } +} + +static void vlcs_defaults(obs_data_t *settings) +{ + obs_data_set_default_bool(settings, S_LOOP, true); + obs_data_set_default_string(settings, S_BEHAVIOR, + S_BEHAVIOR_STOP_RESTART); +} + +static const char *file_filter = + "Media files (*.mp4 *.ts *.mov *.flv *.mkv *.avi " + "*.mp3 *.ogg *.aac *.wav *.webm)"; + +static obs_properties_t *vlcs_properties(void *data) +{ + obs_properties_t *ppts = obs_properties_create(); + struct vlc_source *c = data; + struct dstr path = {0}; + obs_property_t *p; + + obs_properties_add_bool(ppts, S_LOOP, T_LOOP); + + if (c) { + pthread_mutex_lock(&c->mutex); + if (c->files.num) { + struct media_file_data *last = da_end(c->files); + const char *slash; + + dstr_copy(&path, last->path); + dstr_replace(&path, "\\", "/"); + slash = strrchr(path.array, '/'); + if (slash) + dstr_resize(&path, slash - path.array + 1); + } + pthread_mutex_unlock(&c->mutex); + } + + p = obs_properties_add_list(ppts, S_BEHAVIOR, T_BEHAVIOR, + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, T_BEHAVIOR_STOP_RESTART, + S_BEHAVIOR_STOP_RESTART); + obs_property_list_add_string(p, T_BEHAVIOR_PAUSE_UNPAUSE, + S_BEHAVIOR_PAUSE_UNPAUSE); + obs_property_list_add_string(p, T_BEHAVIOR_ALWAYS_PLAY, + S_BEHAVIOR_ALWAYS_PLAY); + + obs_properties_add_editable_list(ppts, S_PLAYLIST, T_PLAYLIST, + OBS_EDITABLE_LIST_TYPE_FILES, file_filter, path.array); + dstr_free(&path); + + return ppts; +} + +struct obs_source_info vlc_source_info = { + .id = "vlc_source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO, + .get_name = vlcs_get_name, + .create = vlcs_create, + .destroy = vlcs_destroy, + .update = vlcs_update, + .get_defaults = vlcs_defaults, + .get_properties = vlcs_properties, + .activate = vlcs_activate, + .deactivate = vlcs_deactivate +};