/** * obs-websocket * Copyright (C) 2016-2017 Stéphane Lepin * Copyright (C) 2017 Brendan Hagan * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see */ #include #include #include #include #include "WSEvents.h" #include "obs-websocket.h" #include "Config.h" #include "Utils.h" #include "rpc/RpcEvent.h" #define STATUS_INTERVAL 2000 const char* sourceTypeToString(obs_source_type type) { switch (type) { case OBS_SOURCE_TYPE_INPUT: return "input"; case OBS_SOURCE_TYPE_SCENE: return "scene"; case OBS_SOURCE_TYPE_TRANSITION: return "transition"; case OBS_SOURCE_TYPE_FILTER: return "filter"; default: return "unknown"; } } template T* calldata_get_pointer(const calldata_t* data, const char* name) { void* ptr = nullptr; calldata_get_ptr(data, name, &ptr); return reinterpret_cast(ptr); } const char* calldata_get_string(const calldata_t* data, const char* name) { const char* value = nullptr; calldata_get_string(data, name, &value); return value; } WSEvents::WSEvents(WSServerPtr srv) : _srv(srv), _streamStarttime(0), _lastBytesSent(0), _lastBytesSentTime(0), HeartbeatIsActive(false), pulse(false) { cpuUsageInfo = os_cpu_usage_info_start(); obs_frontend_add_event_callback(WSEvents::FrontendEventHandler, this); QSpinBox* durationControl = Utils::GetTransitionDurationControl(); connect(durationControl, SIGNAL(valueChanged(int)), this, SLOT(TransitionDurationChanged(int))); connect(&streamStatusTimer, SIGNAL(timeout()), this, SLOT(StreamStatus())); connect(&heartbeatTimer, SIGNAL(timeout()), this, SLOT(Heartbeat())); heartbeatTimer.start(STATUS_INTERVAL); // Connect to signals of all existing sources obs_enum_sources([](void* param, obs_source_t* source) { auto self = reinterpret_cast(param); self->connectSourceSignals(source); return true; }, this); signal_handler_t* coreSignalHandler = obs_get_signal_handler(); if (coreSignalHandler) { signal_handler_connect(coreSignalHandler, "source_create", OnSourceCreate, this); signal_handler_connect(coreSignalHandler, "source_destroy", OnSourceDestroy, this); } } WSEvents::~WSEvents() { signal_handler_t* coreSignalHandler = obs_get_signal_handler(); if (coreSignalHandler) { signal_handler_disconnect(coreSignalHandler, "source_destroy", OnSourceDestroy, this); signal_handler_disconnect(coreSignalHandler, "source_create", OnSourceCreate, this); } // Disconnect from signals of all existing sources obs_enum_sources([](void* param, obs_source_t* source) { auto self = reinterpret_cast(param); self->disconnectSourceSignals(source); return true; }, this); obs_frontend_remove_event_callback(WSEvents::FrontendEventHandler, this); os_cpu_usage_info_destroy(cpuUsageInfo); } void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private_data) { auto owner = reinterpret_cast(private_data); if (!owner->_srv) { return; } switch (event) { case OBS_FRONTEND_EVENT_FINISHED_LOADING: owner->hookTransitionPlaybackEvents(); break; case OBS_FRONTEND_EVENT_SCENE_CHANGED: owner->OnSceneChange(); break; case OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED: owner->OnSceneListChange(); break; case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED: owner->hookTransitionPlaybackEvents(); owner->OnSceneCollectionChange(); break; case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED: owner->OnSceneCollectionListChange(); break; case OBS_FRONTEND_EVENT_TRANSITION_CHANGED: owner->OnTransitionChange(); break; case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED: owner->hookTransitionPlaybackEvents(); owner->OnTransitionListChange(); break; case OBS_FRONTEND_EVENT_PROFILE_CHANGED: owner->OnProfileChange(); break; case OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED: owner->OnProfileListChange(); break; case OBS_FRONTEND_EVENT_STREAMING_STARTING: owner->OnStreamStarting(); break; case OBS_FRONTEND_EVENT_STREAMING_STARTED: owner->streamStatusTimer.start(STATUS_INTERVAL); owner->StreamStatus(); owner->OnStreamStarted(); break; case OBS_FRONTEND_EVENT_STREAMING_STOPPING: owner->streamStatusTimer.stop(); owner->OnStreamStopping(); break; case OBS_FRONTEND_EVENT_STREAMING_STOPPED: owner->OnStreamStopped(); break; case OBS_FRONTEND_EVENT_RECORDING_STARTING: owner->OnRecordingStarting(); break; case OBS_FRONTEND_EVENT_RECORDING_STARTED: owner->OnRecordingStarted(); break; case OBS_FRONTEND_EVENT_RECORDING_STOPPING: owner->OnRecordingStopping(); break; case OBS_FRONTEND_EVENT_RECORDING_STOPPED: owner->OnRecordingStopped(); break; case OBS_FRONTEND_EVENT_RECORDING_PAUSED: owner->OnRecordingPaused(); break; case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED: owner->OnRecordingResumed(); break; case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING: owner->OnReplayStarting(); break; case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED: owner->OnReplayStarted(); break; case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING: owner->OnReplayStopping(); break; case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED: owner->OnReplayStopped(); break; case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED: owner->OnStudioModeSwitched(true); break; case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED: owner->OnStudioModeSwitched(false); break; case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: owner->OnPreviewSceneChanged(); break; case OBS_FRONTEND_EVENT_EXIT: owner->unhookTransitionPlaybackEvents(); owner->OnExit(); break; } } void WSEvents::broadcastUpdate(const char* updateType, obs_data_t* additionalFields = nullptr) { std::optional streamTime; if (obs_frontend_streaming_active()) { streamTime = std::make_optional(getStreamingTime()); } std::optional recordingTime; if (obs_frontend_recording_active()) { recordingTime = std::make_optional(getRecordingTime()); } RpcEvent event(QString(updateType), streamTime, recordingTime, additionalFields); _srv->broadcast(event); } void WSEvents::connectSourceSignals(obs_source_t* source) { if (!source) { return; } // Disconnect everything first to avoid double-binding disconnectSourceSignals(source); obs_source_type sourceType = obs_source_get_type(source); signal_handler_t* sh = obs_source_get_signal_handler(source); signal_handler_connect(sh, "rename", OnSourceRename, this); signal_handler_connect(sh, "mute", OnSourceMuteStateChange, this); signal_handler_connect(sh, "volume", OnSourceVolumeChange, this); signal_handler_connect(sh, "audio_sync", OnSourceAudioSyncOffsetChanged, this); signal_handler_connect(sh, "audio_mixers", OnSourceAudioMixersChanged, this); signal_handler_connect(sh, "audio_activate", OnSourceAudioActivated, this); signal_handler_connect(sh, "audio_deactivate", OnSourceAudioDeactivated, this); signal_handler_connect(sh, "filter_add", OnSourceFilterAdded, this); signal_handler_connect(sh, "filter_remove", OnSourceFilterRemoved, this); signal_handler_connect(sh, "reorder_filters", OnSourceFilterOrderChanged, this); signal_handler_connect(sh, "media_play", OnMediaPlaying, this); signal_handler_connect(sh, "media_pause", OnMediaPaused, this); signal_handler_connect(sh, "media_restart", OnMediaRestarted, this); signal_handler_connect(sh, "media_stopped", OnMediaStopped, this); signal_handler_connect(sh, "media_next", OnMediaNext, this); signal_handler_connect(sh, "media_previous", OnMediaPrevious, this); signal_handler_connect(sh, "media_started", OnMediaStarted, this); signal_handler_connect(sh, "media_ended", OnMediaEnded, this); if (sourceType == OBS_SOURCE_TYPE_SCENE) { signal_handler_connect(sh, "reorder", OnSceneReordered, this); signal_handler_connect(sh, "item_add", OnSceneItemAdd, this); signal_handler_connect(sh, "item_remove", OnSceneItemDelete, this); signal_handler_connect(sh, "item_visible", OnSceneItemVisibilityChanged, this); signal_handler_connect(sh, "item_locked", OnSceneItemLockChanged, this); signal_handler_connect(sh, "item_transform", OnSceneItemTransform, this); signal_handler_connect(sh, "item_select", OnSceneItemSelected, this); signal_handler_connect(sh, "item_deselect", OnSceneItemDeselected, this); } } void WSEvents::disconnectSourceSignals(obs_source_t* source) { if (!source) { return; } signal_handler_t* sh = obs_source_get_signal_handler(source); signal_handler_disconnect(sh, "rename", OnSourceRename, this); signal_handler_disconnect(sh, "mute", OnSourceMuteStateChange, this); signal_handler_disconnect(sh, "volume", OnSourceVolumeChange, this); signal_handler_disconnect(sh, "audio_sync", OnSourceAudioSyncOffsetChanged, this); signal_handler_disconnect(sh, "audio_mixers", OnSourceAudioMixersChanged, this); signal_handler_disconnect(sh, "audio_activate", OnSourceAudioActivated, this); signal_handler_disconnect(sh, "audio_deactivate", OnSourceAudioDeactivated, this); signal_handler_disconnect(sh, "filter_add", OnSourceFilterAdded, this); signal_handler_disconnect(sh, "filter_remove", OnSourceFilterRemoved, this); signal_handler_disconnect(sh, "reorder_filters", OnSourceFilterOrderChanged, this); signal_handler_disconnect(sh, "reorder", OnSceneReordered, this); signal_handler_disconnect(sh, "item_add", OnSceneItemAdd, this); signal_handler_disconnect(sh, "item_remove", OnSceneItemDelete, this); signal_handler_disconnect(sh, "item_visible", OnSceneItemVisibilityChanged, this); signal_handler_disconnect(sh, "item_locked", OnSceneItemLockChanged, this); signal_handler_disconnect(sh, "item_transform", OnSceneItemTransform, this); signal_handler_disconnect(sh, "item_select", OnSceneItemSelected, this); signal_handler_disconnect(sh, "item_deselect", OnSceneItemDeselected, this); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); signal_handler_disconnect(sh, "media_play", OnMediaPlaying, this); signal_handler_disconnect(sh, "media_pause", OnMediaPaused, this); signal_handler_disconnect(sh, "media_restart", OnMediaRestarted, this); signal_handler_disconnect(sh, "media_stopped", OnMediaStopped, this); signal_handler_disconnect(sh, "media_next", OnMediaNext, this); signal_handler_disconnect(sh, "media_previous", OnMediaPrevious, this); signal_handler_disconnect(sh, "media_started", OnMediaStarted, this); signal_handler_disconnect(sh, "media_ended", OnMediaEnded, this); } void WSEvents::connectFilterSignals(obs_source_t* filter) { if (!filter) { return; } signal_handler_t* sh = obs_source_get_signal_handler(filter); signal_handler_connect(sh, "enable", OnSourceFilterVisibilityChanged, this); } void WSEvents::disconnectFilterSignals(obs_source_t* filter) { if (!filter) { return; } signal_handler_t* sh = obs_source_get_signal_handler(filter); signal_handler_disconnect(sh, "enable", OnSourceFilterVisibilityChanged, this); } void WSEvents::hookTransitionPlaybackEvents() { obs_frontend_source_list transitions = {}; obs_frontend_get_transitions(&transitions); for (uint i = 0; i < transitions.sources.num; i++) { obs_source_t* transition = transitions.sources.array[i]; signal_handler_t* sh = obs_source_get_signal_handler(transition); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); signal_handler_connect(sh, "transition_start", OnTransitionBegin, this); signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); signal_handler_connect(sh, "transition_stop", OnTransitionEnd, this); signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); signal_handler_connect(sh, "transition_video_stop", OnTransitionVideoEnd, this); } obs_frontend_source_list_free(&transitions); } void WSEvents::unhookTransitionPlaybackEvents() { obs_frontend_source_list transitions = {}; obs_frontend_get_transitions(&transitions); for (uint i = 0; i < transitions.sources.num; i++) { obs_source_t* transition = transitions.sources.array[i]; signal_handler_t* sh = obs_source_get_signal_handler(transition); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); } obs_frontend_source_list_free(&transitions); } uint64_t getOutputRunningTime(obs_output_t* output) { if (!output || !obs_output_active(output)) { return 0; } video_t* video = obs_output_video(output); uint64_t frameTimeNs = video_output_get_frame_time(video); int totalFrames = obs_output_get_total_frames(output); return (((uint64_t)totalFrames) * frameTimeNs); } uint64_t WSEvents::getStreamingTime() { OBSOutputAutoRelease streamingOutput = obs_frontend_get_streaming_output(); return getOutputRunningTime(streamingOutput); } uint64_t WSEvents::getRecordingTime() { OBSOutputAutoRelease recordingOutput = obs_frontend_get_recording_output(); return getOutputRunningTime(recordingOutput); } QString WSEvents::getStreamingTimecode() { return Utils::nsToTimestamp(getStreamingTime()); } QString WSEvents::getRecordingTimecode() { return Utils::nsToTimestamp(getRecordingTime()); } OBSDataAutoRelease getMediaSourceData(calldata_t* data) { OBSDataAutoRelease fields = obs_data_create(); OBSSource source = calldata_get_pointer(data, "source"); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_string(fields, "sourceTypeId", obs_source_get_id(source)); return fields; } /** * Indicates a scene change. * * @return {String} `scene-name` The new scene. * @return {Array} `sources` List of scene items in the new scene. Same specification as [`GetCurrentScene`](#getcurrentscene). * * @api events * @name SwitchScenes * @category scenes * @since 0.3 */ void WSEvents::OnSceneChange() { OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene(); OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(currentScene); OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "scene-name", obs_source_get_name(currentScene)); obs_data_set_array(data, "sources", sceneItems); broadcastUpdate("SwitchScenes", data); } /** * The scene list has been modified. * Scenes have been added, removed, or renamed. * * @api events * @name ScenesChanged * @category scenes * @since 0.3 */ void WSEvents::OnSceneListChange() { broadcastUpdate("ScenesChanged"); } /** * Triggered when switching to another scene collection or when renaming the current scene collection. * * @api events * @name SceneCollectionChanged * @category scenes * @since 4.0.0 */ void WSEvents::OnSceneCollectionChange() { broadcastUpdate("SceneCollectionChanged"); OnTransitionListChange(); OnTransitionChange(); OnSceneListChange(); OnSceneChange(); } /** * Triggered when a scene collection is created, added, renamed, or removed. * * @api events * @name SceneCollectionListChanged * @category scenes * @since 4.0.0 */ void WSEvents::OnSceneCollectionListChange() { broadcastUpdate("SceneCollectionListChanged"); } /** * The active transition has been changed. * * @return {String} `transition-name` The name of the new active transition. * * @api events * @name SwitchTransition * @category transitions * @since 4.0.0 */ void WSEvents::OnTransitionChange() { OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition(); OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "transition-name", obs_source_get_name(currentTransition)); broadcastUpdate("SwitchTransition", data); } /** * The list of available transitions has been modified. * Transitions have been added, removed, or renamed. * * @api events * @name TransitionListChanged * @category transitions * @since 4.0.0 */ void WSEvents::OnTransitionListChange() { broadcastUpdate("TransitionListChanged"); } /** * Triggered when switching to another profile or when renaming the current profile. * * @api events * @name ProfileChanged * @category profiles * @since 4.0.0 */ void WSEvents::OnProfileChange() { broadcastUpdate("ProfileChanged"); } /** * Triggered when a profile is created, added, renamed, or removed. * * @api events * @name ProfileListChanged * @category profiles * @since 4.0.0 */ void WSEvents::OnProfileListChange() { broadcastUpdate("ProfileListChanged"); } /** * A request to start streaming has been issued. * * @return {boolean} `preview-only` Always false (retrocompatibility). * * @api events * @name StreamStarting * @category streaming * @since 0.3 */ void WSEvents::OnStreamStarting() { OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "preview-only", false); broadcastUpdate("StreamStarting", data); } /** * Streaming started successfully. * * @api events * @name StreamStarted * @category streaming * @since 0.3 */ void WSEvents::OnStreamStarted() { _streamStarttime = os_gettime_ns(); _lastBytesSent = 0; broadcastUpdate("StreamStarted"); } /** * A request to stop streaming has been issued. * * @return {boolean} `preview-only` Always false (retrocompatibility). * * @api events * @name StreamStopping * @category streaming * @since 0.3 */ void WSEvents::OnStreamStopping() { OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "preview-only", false); broadcastUpdate("StreamStopping", data); } /** * Streaming stopped successfully. * * @api events * @name StreamStopped * @category streaming * @since 0.3 */ void WSEvents::OnStreamStopped() { _streamStarttime = 0; broadcastUpdate("StreamStopped"); } /** * A request to start recording has been issued. * * @api events * @name RecordingStarting * @category recording * @since 0.3 */ void WSEvents::OnRecordingStarting() { broadcastUpdate("RecordingStarting"); } /** * Recording started successfully. * * @api events * @name RecordingStarted * @category recording * @since 0.3 */ void WSEvents::OnRecordingStarted() { broadcastUpdate("RecordingStarted"); } /** * A request to stop recording has been issued. * * @api events * @name RecordingStopping * @category recording * @since 0.3 */ void WSEvents::OnRecordingStopping() { broadcastUpdate("RecordingStopping"); } /** * Recording stopped successfully. * * @api events * @name RecordingStopped * @category recording * @since 0.3 */ void WSEvents::OnRecordingStopped() { broadcastUpdate("RecordingStopped"); } /** * Current recording paused * * @api events * @name RecordingPaused * @category recording * @since 4.7.0 */ void WSEvents::OnRecordingPaused() { broadcastUpdate("RecordingPaused"); } /** * Current recording resumed * * @api events * @name RecordingResumed * @category recording * @since 4.7.0 */ void WSEvents::OnRecordingResumed() { broadcastUpdate("RecordingResumed"); } /** * A request to start the replay buffer has been issued. * * @api events * @name ReplayStarting * @category replay buffer * @since 4.2.0 */ void WSEvents::OnReplayStarting() { broadcastUpdate("ReplayStarting"); } /** * Replay Buffer started successfully * * @api events * @name ReplayStarted * @category replay buffer * @since 4.2.0 */ void WSEvents::OnReplayStarted() { broadcastUpdate("ReplayStarted"); } /** * A request to stop the replay buffer has been issued. * * @api events * @name ReplayStopping * @category replay buffer * @since 4.2.0 */ void WSEvents::OnReplayStopping() { broadcastUpdate("ReplayStopping"); } /** * Replay Buffer stopped successfully * * @api events * @name ReplayStopped * @category replay buffer * @since 4.2.0 */ void WSEvents::OnReplayStopped() { broadcastUpdate("ReplayStopped"); } /** * OBS is exiting. * * @api events * @name Exiting * @category other * @since 0.3 */ void WSEvents::OnExit() { broadcastUpdate("Exiting"); } /** * Emit every 2 seconds. * * @return {boolean} `streaming` Current streaming state. * @return {boolean} `recording` Current recording state. * @return {boolean} `replay-buffer-active` Replay Buffer status * @return {int} `bytes-per-sec` Amount of data per second (in bytes) transmitted by the stream encoder. * @return {int} `kbits-per-sec` Amount of data per second (in kilobits) transmitted by the stream encoder. * @return {double} `strain` Percentage of dropped frames. * @return {int} `total-stream-time` Total time (in seconds) since the stream started. * @return {int} `num-total-frames` Total number of frames transmitted since the stream started. * @return {int} `num-dropped-frames` Number of frames dropped by the encoder since the stream started. * @return {double} `fps` Current framerate. * @return {int} `render-total-frames` Number of frames rendered * @return {int} `render-missed-frames` Number of frames missed due to rendering lag * @return {int} `output-total-frames` Number of frames outputted * @return {int} `output-skipped-frames` Number of frames skipped due to encoding lag * @return {double} `average-frame-time` Average frame time (in milliseconds) * @return {double} `cpu-usage` Current CPU usage (percentage) * @return {double} `memory-usage` Current RAM usage (in megabytes) * @return {double} `free-disk-space` Free recording disk space (in megabytes) * @return {boolean} `preview-only` Always false (retrocompatibility). * * @api events * @name StreamStatus * @category streaming * @since 0.3 */ void WSEvents::StreamStatus() { bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); bool recordingPaused = obs_frontend_recording_paused(); bool replayBufferActive = obs_frontend_replay_buffer_active(); OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output(); if (!streamOutput || !streamingActive) { return; } uint64_t bytesSent = obs_output_get_total_bytes(streamOutput); uint64_t bytesSentTime = os_gettime_ns(); if (bytesSent < _lastBytesSent) bytesSent = 0; if (bytesSent == 0) _lastBytesSent = 0; uint64_t bytesBetween = bytesSent - _lastBytesSent; double timePassed = double(bytesSentTime - _lastBytesSentTime) / 1000000000.0; uint64_t bytesPerSec = bytesBetween / timePassed; _lastBytesSent = bytesSent; _lastBytesSentTime = bytesSentTime; uint64_t totalStreamTime = (getStreamingTime() / 1000000000ULL); int totalFrames = obs_output_get_total_frames(streamOutput); int droppedFrames = obs_output_get_frames_dropped(streamOutput); float strain = obs_output_get_congestion(streamOutput); OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "streaming", streamingActive); obs_data_set_bool(data, "recording", recordingActive); obs_data_set_bool(data, "recording-paused", recordingPaused); obs_data_set_bool(data, "replay-buffer-active", replayBufferActive); obs_data_set_int(data, "bytes-per-sec", bytesPerSec); obs_data_set_int(data, "kbits-per-sec", (bytesPerSec * 8) / 1024); obs_data_set_int(data, "total-stream-time", totalStreamTime); obs_data_set_int(data, "num-total-frames", totalFrames); obs_data_set_int(data, "num-dropped-frames", droppedFrames); obs_data_set_double(data, "strain", strain); // `stats` contains fps, cpu usage, memory usage, render missed frames, ... OBSDataAutoRelease stats = GetStats(); obs_data_apply(data, stats); obs_data_set_bool(data, "preview-only", false); // Retrocompat with OBSRemote broadcastUpdate("StreamStatus", data); } /** * Emitted every 2 seconds after enabling it by calling SetHeartbeat. * * @return {boolean} `pulse` Toggles between every JSON message as an "I am alive" indicator. * @return {string (optional)} `current-profile` Current active profile. * @return {string (optional)} `current-scene` Current active scene. * @return {boolean (optional)} `streaming` Current streaming state. * @return {int (optional)} `total-stream-time` Total time (in seconds) since the stream started. * @return {int (optional)} `total-stream-bytes` Total bytes sent since the stream started. * @return {int (optional)} `total-stream-frames` Total frames streamed since the stream started. * @return {boolean (optional)} `recording` Current recording state. * @return {int (optional)} `total-record-time` Total time (in seconds) since recording started. * @return {int (optional)} `total-record-bytes` Total bytes recorded since the recording started. * @return {int (optional)} `total-record-frames` Total frames recorded since the recording started. * @return {OBSStats} `stats` OBS Stats * * @api events * @name Heartbeat * @category general */ void WSEvents::Heartbeat() { if (!HeartbeatIsActive) return; bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); bool recordingPaused = obs_frontend_recording_paused(); OBSDataAutoRelease data = obs_data_create(); OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output(); OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output(); pulse = !pulse; obs_data_set_bool(data, "pulse", pulse); char* currentProfile = obs_frontend_get_current_profile(); obs_data_set_string(data, "current-profile", currentProfile); bfree(currentProfile); OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene(); obs_data_set_string(data, "current-scene", obs_source_get_name(currentScene)); obs_data_set_bool(data, "streaming", streamingActive); if (streamingActive) { obs_data_set_int(data, "total-stream-time", (getStreamingTime() / 1000000000ULL)); obs_data_set_int(data, "total-stream-bytes", (uint64_t)obs_output_get_total_bytes(streamOutput)); obs_data_set_int(data, "total-stream-frames", obs_output_get_total_frames(streamOutput)); } obs_data_set_bool(data, "recording", recordingActive); obs_data_set_bool(data, "recording-paused", recordingPaused); if (recordingActive) { obs_data_set_int(data, "total-record-time", (getRecordingTime() / 1000000000ULL)); obs_data_set_int(data, "total-record-bytes", (uint64_t)obs_output_get_total_bytes(recordOutput)); obs_data_set_int(data, "total-record-frames", obs_output_get_total_frames(recordOutput)); } OBSDataAutoRelease stats = GetStats(); obs_data_set_obj(data, "stats", stats); broadcastUpdate("Heartbeat", data); } /** * The active transition duration has been changed. * * @return {int} `new-duration` New transition duration. * * @api events * @name TransitionDurationChanged * @category transitions * @since 4.0.0 */ void WSEvents::TransitionDurationChanged(int ms) { OBSDataAutoRelease fields = obs_data_create(); obs_data_set_int(fields, "new-duration", ms); broadcastUpdate("TransitionDurationChanged", fields); } /** * A transition (other than "cut") has begun. * * @return {String} `name` Transition name. * @return {String} `type` Transition type. * @return {int} `duration` Transition duration (in milliseconds). * Will be -1 for any transition with a fixed duration, * such as a Stinger, due to limitations of the OBS API. * @return {String} `from-scene` Source scene of the transition * @return {String} `to-scene` Destination scene of the transition * * @api events * @name TransitionBegin * @category transitions * @since 4.0.0 */ void WSEvents::OnTransitionBegin(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); OBSSource transition = calldata_get_pointer(data, "source"); if (!transition) { return; } OBSDataAutoRelease fields = Utils::GetTransitionData(transition); instance->broadcastUpdate("TransitionBegin", fields); } /** * A transition (other than "cut") has ended. * Please note that the `from-scene` field is not available in TransitionEnd. * * @return {String} `name` Transition name. * @return {String} `type` Transition type. * @return {int} `duration` Transition duration (in milliseconds). * @return {String} `to-scene` Destination scene of the transition * * @api events * @name TransitionEnd * @category transitions * @since 4.8.0 */ void WSEvents::OnTransitionEnd(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); OBSSource transition = calldata_get_pointer(data, "source"); if (!transition) { return; } OBSDataAutoRelease fields = Utils::GetTransitionData(transition); instance->broadcastUpdate("TransitionEnd", fields); } /** * A stinger transition has finished playing its video. * * @return {String} `name` Transition name. * @return {String} `type` Transition type. * @return {int} `duration` Transition duration (in milliseconds). * @return {String} `from-scene` Source scene of the transition * @return {String} `to-scene` Destination scene of the transition * * @api events * @name TransitionVideoEnd * @category transitions * @since 4.8.0 */ void WSEvents::OnTransitionVideoEnd(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); OBSSource transition = calldata_get_pointer(data, "source"); if (!transition) { return; } OBSDataAutoRelease fields = Utils::GetTransitionData(transition); instance->broadcastUpdate("TransitionVideoEnd", fields); } /** * A source has been created. A source can be an input, a scene or a transition. * * @return {String} `sourceName` Source name * @return {String} `sourceType` Source type. Can be "input", "scene", "transition" or "filter". * @return {String} `sourceKind` Source kind. * @return {Object} `sourceSettings` Source settings * * @api events * @name SourceCreated * @category sources * @since 4.6.0 */ void WSEvents::OnSourceCreate(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } self->connectSourceSignals(source); OBSDataAutoRelease sourceSettings = obs_source_get_settings(source); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_string(fields, "sourceType", sourceTypeToString(obs_source_get_type(source)) ); obs_data_set_string(fields, "sourceKind", obs_source_get_id(source)); obs_data_set_obj(fields, "sourceSettings", sourceSettings); self->broadcastUpdate("SourceCreated", fields); } /** * A source has been destroyed/removed. A source can be an input, a scene or a transition. * * @return {String} `sourceName` Source name * @return {String} `sourceType` Source type. Can be "input", "scene", "transition" or "filter". * @return {String} `sourceKind` Source kind. * * @api events * @name SourceDestroyed * @category sources * @since 4.6.0 */ void WSEvents::OnSourceDestroy(void* param, calldata_t* data) { auto self = reinterpret_cast(param); obs_source_t* source = calldata_get_pointer(data, "source"); if (!source) { return; } self->disconnectSourceSignals(source); obs_source_type sourceType = obs_source_get_type(source); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_string(fields, "sourceType", sourceTypeToString(sourceType)); obs_data_set_string(fields, "sourceKind", obs_source_get_id(source)); self->broadcastUpdate("SourceDestroyed", fields); } /** * The volume of a source has changed. * * @return {String} `sourceName` Source name * @return {float} `volume` Source volume * * @api events * @name SourceVolumeChanged * @category sources * @since 4.6.0 */ void WSEvents::OnSourceVolumeChange(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } double volume = 0; if (!calldata_get_float(data, "volume", &volume)) { return; } OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_double(fields, "volume", volume); self->broadcastUpdate("SourceVolumeChanged", fields); } /** * A source has been muted or unmuted. * * @return {String} `sourceName` Source name * @return {boolean} `muted` Mute status of the source * * @api events * @name SourceMuteStateChanged * @category sources * @since 4.6.0 */ void WSEvents::OnSourceMuteStateChange(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } bool muted = false; if (!calldata_get_bool(data, "muted", &muted)) { return; } OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_bool(fields, "muted", muted); self->broadcastUpdate("SourceMuteStateChanged", fields); } /** * A source has removed audio. * * @return {String} `sourceName` Source name * * @api events * @name SourceAudioDeactivated * @category sources * @since unreleased */ void WSEvents::OnSourceAudioDeactivated(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); self->broadcastUpdate("SourceAudioDeactivated", fields); } /** * A source has added audio. * * @return {String} `sourceName` Source name * * @api events * @name SourceAudioActivated * @category sources * @since unreleased */ void WSEvents::OnSourceAudioActivated(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); self->broadcastUpdate("SourceAudioActivated", fields); } /** * The audio sync offset of a source has changed. * * @return {String} `sourceName` Source name * @return {int} `syncOffset` Audio sync offset of the source (in nanoseconds) * * @api events * @name SourceAudioSyncOffsetChanged * @category sources * @since 4.6.0 */ void WSEvents::OnSourceAudioSyncOffsetChanged(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } long long syncOffset; if (!calldata_get_int(data, "offset", &syncOffset)) { return; } OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_int(fields, "syncOffset", (int)syncOffset); self->broadcastUpdate("SourceAudioSyncOffsetChanged", fields); } /** * Audio mixer routing changed on a source. * * @return {String} `sourceName` Source name * @return {Array} `mixers` Routing status of the source for each audio mixer (array of 6 values) * @return {int} `mixers.*.id` Mixer number * @return {boolean} `mixers.*.enabled` Routing status * @return {String} `hexMixersValue` Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value * * @api events * @name SourceAudioMixersChanged * @category sources * @since 4.6.0 */ void WSEvents::OnSourceAudioMixersChanged(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } long long audioMixers; if (!calldata_get_int(data, "mixers", &audioMixers)) { return; } OBSDataArrayAutoRelease mixers = obs_data_array_create(); for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { OBSDataAutoRelease item = obs_data_create(); obs_data_set_int(item, "id", i + 1); obs_data_set_bool(item, "enabled", (1 << i) & audioMixers); obs_data_array_push_back(mixers, item); } const QString hexValue = QString::number(audioMixers, 16).toUpper().prepend("0x"); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_array(fields, "mixers", mixers); obs_data_set_string(fields, "hexMixersValue", hexValue.toUtf8()); self->broadcastUpdate("SourceAudioMixersChanged", fields); } /** * A source has been renamed. * * @return {String} `previousName` Previous source name * @return {String} `newName` New source name * @return {String} `sourceType` Type of source (input, scene, filter, transition) * * @api events * @name SourceRenamed * @category sources * @since 4.6.0 */ void WSEvents::OnSourceRename(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } const char* newName = calldata_get_string(data, "new_name"); if (!newName) { return; } const char* previousName = calldata_get_string(data, "prev_name"); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "previousName", previousName); obs_data_set_string(fields, "newName", newName); obs_data_set_string(fields, "sourceType", sourceTypeToString(obs_source_get_type(source))); // TODO: Split into dedicated events for source/scene. Only doing it this way for backwards compatability until 5.0 self->broadcastUpdate("SourceRenamed", fields); } /** * A filter was added to a source. * * @return {String} `sourceName` Source name * @return {String} `filterName` Filter name * @return {String} `filterType` Filter type * @return {Object} `filterSettings` Filter settings * * @api events * @name SourceFilterAdded * @category sources * @since 4.6.0 */ void WSEvents::OnSourceFilterAdded(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } OBSSource filter = calldata_get_pointer(data, "filter"); if (!filter) { return; } self->connectFilterSignals(filter); OBSDataAutoRelease filterSettings = obs_source_get_settings(filter); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_string(fields, "filterName", obs_source_get_name(filter)); obs_data_set_string(fields, "filterType", obs_source_get_id(filter)); obs_data_set_obj(fields, "filterSettings", filterSettings); self->broadcastUpdate("SourceFilterAdded", fields); } /** * A filter was removed from a source. * * @return {String} `sourceName` Source name * @return {String} `filterName` Filter name * @return {String} `filterType` Filter type * * @api events * @name SourceFilterRemoved * @category sources * @since 4.6.0 */ void WSEvents::OnSourceFilterRemoved(void* param, calldata_t* data) { auto self = reinterpret_cast(param); obs_source_t* source = calldata_get_pointer(data, "source"); if (!source) { return; } obs_source_t* filter = calldata_get_pointer(data, "filter"); if (!filter) { return; } self->disconnectFilterSignals(filter); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_string(fields, "filterName", obs_source_get_name(filter)); obs_data_set_string(fields, "filterType", obs_source_get_id(filter)); self->broadcastUpdate("SourceFilterRemoved", fields); } /** * The visibility/enabled state of a filter changed * * @return {String} `sourceName` Source name * @return {String} `filterName` Filter name * @return {Boolean} `filterEnabled` New filter state * * @api events * @name SourceFilterVisibilityChanged * @category sources * @since 4.7.0 */ void WSEvents::OnSourceFilterVisibilityChanged(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } OBSSource parent = obs_filter_get_parent(source); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(parent)); obs_data_set_string(fields, "filterName", obs_source_get_name(source)); obs_data_set_bool(fields, "filterEnabled", obs_source_enabled(source)); self->broadcastUpdate("SourceFilterVisibilityChanged", fields); } /** * Filters in a source have been reordered. * * @return {String} `sourceName` Source name * @return {Array} `filters` Ordered Filters list * @return {String} `filters.*.name` Filter name * @return {String} `filters.*.type` Filter type * * @api events * @name SourceFiltersReordered * @category sources * @since 4.6.0 */ void WSEvents::OnSourceFilterOrderChanged(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSSource source = calldata_get_pointer(data, "source"); if (!source) { return; } OBSDataArrayAutoRelease filters = Utils::GetSourceFiltersList(source, false); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); obs_data_set_array(fields, "filters", filters); self->broadcastUpdate("SourceFiltersReordered", fields); } /** * A media source has started playing. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaPlaying * @category media * @since 4.9.0 */ void WSEvents::OnMediaPlaying(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaPlaying", fields); } /** * A media source has been paused. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaPaused * @category media * @since 4.9.0 */ void WSEvents::OnMediaPaused(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaPaused", fields); } /** * A media source has been restarted. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaRestarted * @category media * @since 4.9.0 */ void WSEvents::OnMediaRestarted(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaRestarted", fields); } /** * A media source has been stopped. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaStopped * @category media * @since 4.9.0 */ void WSEvents::OnMediaStopped(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaStopped", fields); } /** * A media source has gone to the next item in the playlist. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaNext * @category media * @since 4.9.0 */ void WSEvents::OnMediaNext(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaNext", fields); } /** * A media source has gone to the previous item in the playlist. * * Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaPrevious * @category media * @since 4.9.0 */ void WSEvents::OnMediaPrevious(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaPrevious", fields); } /** * A media source has been started. * * Note: These events are emitted by the OBS sources themselves. For example when the media file starts playing. The behavior depends on the type of media source being used. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaStarted * @category media * @since 4.9.0 */ void WSEvents::OnMediaStarted(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaStarted", fields); } /** * A media source has ended. * * Note: These events are emitted by the OBS sources themselves. For example when the media file ends. The behavior depends on the type of media source being used. * * @return {String} `sourceName` Source name * @return {String} `sourceTypeId` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) * * @api events * @name MediaEnded * @category media * @since 4.9.0 */ void WSEvents::OnMediaEnded(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSDataAutoRelease fields = getMediaSourceData(data); self->broadcastUpdate("MediaEnded", fields); } /** * Scene items have been reordered. * * @return {String} `scene-name` Name of the scene where items have been reordered. * @return {Array} `scene-items` Ordered list of scene items * @return {String} `scene-items.*.source-name` Item source name * @return {int} `scene-items.*.item-id` Scene item unique ID * * @api events * @name SourceOrderChanged * @category scene items * @since 4.0.0 */ void WSEvents::OnSceneReordered(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); OBSScene scene = calldata_get_pointer(data, "scene"); if (!scene) { return; } OBSDataArrayAutoRelease sceneItems = obs_data_array_create(); obs_scene_enum_items(scene, [](obs_scene_t* scene, obs_sceneitem_t* sceneItem, void* param) { obs_data_array_t* sceneItems = reinterpret_cast(param); OBSSource itemSource = obs_sceneitem_get_source(sceneItem); OBSDataAutoRelease item = obs_data_create(); obs_data_set_string(item, "source-name", obs_source_get_name(itemSource)); obs_data_set_int(item, "item-id", obs_sceneitem_get_id(sceneItem)); obs_data_array_push_back(sceneItems, item); return true; }, sceneItems); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", obs_source_get_name(obs_scene_get_source(scene))); obs_data_set_array(fields, "scene-items", sceneItems); instance->broadcastUpdate("SourceOrderChanged", fields); } /** * An item has been added to the current scene. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item added to the scene. * @return {int} `item-id` Scene item ID * * @api events * @name SceneItemAdded * @category scene items * @since 4.0.0 */ void WSEvents::OnSceneItemAdd(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); obs_scene_t* scene = nullptr; calldata_get_ptr(data, "scene", &scene); obs_sceneitem_t* sceneItem = nullptr; calldata_get_ptr(data, "item", &sceneItem); const char* sceneName = obs_source_get_name(obs_scene_get_source(scene)); const char* sceneItemName = obs_source_get_name(obs_sceneitem_get_source(sceneItem)); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", sceneName); obs_data_set_string(fields, "item-name", sceneItemName); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); instance->broadcastUpdate("SceneItemAdded", fields); } /** * An item has been removed from the current scene. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item removed from the scene. * @return {int} `item-id` Scene item ID * * @api events * @name SceneItemRemoved * @category scene items * @since 4.0.0 */ void WSEvents::OnSceneItemDelete(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); obs_scene_t* scene = nullptr; calldata_get_ptr(data, "scene", &scene); obs_sceneitem_t* sceneItem = nullptr; calldata_get_ptr(data, "item", &sceneItem); const char* sceneName = obs_source_get_name(obs_scene_get_source(scene)); const char* sceneItemName = obs_source_get_name(obs_sceneitem_get_source(sceneItem)); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", sceneName); obs_data_set_string(fields, "item-name", sceneItemName); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); instance->broadcastUpdate("SceneItemRemoved", fields); } /** * An item's visibility has been toggled. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item in the scene. * @return {int} `item-id` Scene item ID * @return {boolean} `item-visible` New visibility state of the item. * * @api events * @name SceneItemVisibilityChanged * @category scene items * @since 4.0.0 */ void WSEvents::OnSceneItemVisibilityChanged(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); obs_scene_t* scene = nullptr; calldata_get_ptr(data, "scene", &scene); obs_sceneitem_t* sceneItem = nullptr; calldata_get_ptr(data, "item", &sceneItem); bool visible = false; calldata_get_bool(data, "visible", &visible); const char* sceneName = obs_source_get_name(obs_scene_get_source(scene)); const char* sceneItemName = obs_source_get_name(obs_sceneitem_get_source(sceneItem)); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", sceneName); obs_data_set_string(fields, "item-name", sceneItemName); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); obs_data_set_bool(fields, "item-visible", visible); instance->broadcastUpdate("SceneItemVisibilityChanged", fields); } /** * An item's locked status has been toggled. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item in the scene. * @return {int} `item-id` Scene item ID * @return {boolean} `item-locked` New locked state of the item. * * @api events * @name SceneItemLockChanged * @category scene items * @since 4.8.0 */ void WSEvents::OnSceneItemLockChanged(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); obs_scene_t* scene = nullptr; calldata_get_ptr(data, "scene", &scene); obs_sceneitem_t* sceneItem = nullptr; calldata_get_ptr(data, "item", &sceneItem); bool locked = false; calldata_get_bool(data, "locked", &locked); const char* sceneName = obs_source_get_name(obs_scene_get_source(scene)); const char* sceneItemName = obs_source_get_name(obs_sceneitem_get_source(sceneItem)); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", sceneName); obs_data_set_string(fields, "item-name", sceneItemName); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); obs_data_set_bool(fields, "item-locked", locked); instance->broadcastUpdate("SceneItemLockChanged", fields); } /** * An item's transform has been changed. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item in the scene. * @return {int} `item-id` Scene item ID * @return {SceneItemTransform} `transform` Scene item transform properties * * @api events * @name SceneItemTransformChanged * @category scene items * @since 4.6.0 */ void WSEvents::OnSceneItemTransform(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); obs_scene_t* scene = nullptr; calldata_get_ptr(data, "scene", &scene); obs_sceneitem_t* sceneItem = nullptr; calldata_get_ptr(data, "item", &sceneItem); const char* sceneName = obs_source_get_name(obs_scene_get_source(scene)); const char* sceneItemName = obs_source_get_name(obs_sceneitem_get_source(sceneItem)); OBSDataAutoRelease transform = Utils::GetSceneItemPropertiesData(sceneItem); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", sceneName); obs_data_set_string(fields, "item-name", sceneItemName); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); obs_data_set_obj(fields, "transform", transform); instance->broadcastUpdate("SceneItemTransformChanged", fields); } /** * A scene item is selected. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item in the scene. * @return {int} `item-id` Name of the item in the scene. * * @api events * @name SceneItemSelected * @category scene items * @since 4.6.0 */ void WSEvents::OnSceneItemSelected(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSScene scene = calldata_get_pointer(data, "scene"); if (!scene) { return; } OBSSceneItem item = calldata_get_pointer(data, "item"); if (!item) { return; } OBSSource sceneSource = obs_scene_get_source(scene); OBSSource itemSource = obs_sceneitem_get_source(item); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", obs_source_get_name(sceneSource)); obs_data_set_string(fields, "item-name", obs_source_get_name(itemSource)); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(item)); self->broadcastUpdate("SceneItemSelected", fields); } /** * A scene item is deselected. * * @return {String} `scene-name` Name of the scene. * @return {String} `item-name` Name of the item in the scene. * @return {int} `item-id` Name of the item in the scene. * * @api events * @name SceneItemDeselected * @category scene items * @since 4.6.0 */ void WSEvents::OnSceneItemDeselected(void* param, calldata_t* data) { auto self = reinterpret_cast(param); OBSScene scene = calldata_get_pointer(data, "scene"); if (!scene) { return; } OBSSceneItem item = calldata_get_pointer(data, "item"); if (!item) { return; } OBSSource sceneSource = obs_scene_get_source(scene); OBSSource itemSource = obs_sceneitem_get_source(item); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "scene-name", obs_source_get_name(sceneSource)); obs_data_set_string(fields, "item-name", obs_source_get_name(itemSource)); obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(item)); self->broadcastUpdate("SceneItemDeselected", fields); } /** * The selected preview scene has changed (only available in Studio Mode). * * @return {String} `scene-name` Name of the scene being previewed. * @return {Array} `sources` List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene). * * @api events * @name PreviewSceneChanged * @category studio mode * @since 4.1.0 */ void WSEvents::OnPreviewSceneChanged() { if (obs_frontend_preview_program_mode_active()) { OBSSourceAutoRelease scene = obs_frontend_get_current_preview_scene(); if (!scene) return; OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(scene); OBSDataAutoRelease data = obs_data_create(); obs_data_set_string(data, "scene-name", obs_source_get_name(scene)); obs_data_set_array(data, "sources", sceneItems); broadcastUpdate("PreviewSceneChanged", data); } } /** * Studio Mode has been enabled or disabled. * * @return {boolean} `new-state` The new enabled state of Studio Mode. * * @api events * @name StudioModeSwitched * @category studio mode * @since 4.1.0 */ void WSEvents::OnStudioModeSwitched(bool checked) { OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "new-state", checked); broadcastUpdate("StudioModeSwitched", data); } /** * A custom broadcast message was received * * @return {String} `realm` Identifier provided by the sender * @return {Object} `data` User-defined data * * @api events * @name BroadcastCustomMessage * @category general * @since 4.7.0 */ void WSEvents::OnBroadcastCustomMessage(QString realm, obs_data_t* data) { OBSDataAutoRelease broadcastData = obs_data_create(); obs_data_set_string(broadcastData, "realm", realm.toUtf8().constData()); obs_data_set_obj(broadcastData, "data", data); broadcastUpdate("BroadcastCustomMessage", broadcastData); } /** * @typedef {Object} `OBSStats` * @property {double} `fps` Current framerate. * @property {int} `render-total-frames` Number of frames rendered * @property {int} `render-missed-frames` Number of frames missed due to rendering lag * @property {int} `output-total-frames` Number of frames outputted * @property {int} `output-skipped-frames` Number of frames skipped due to encoding lag * @property {double} `average-frame-time` Average frame render time (in milliseconds) * @property {double} `cpu-usage` Current CPU usage (percentage) * @property {double} `memory-usage` Current RAM usage (in megabytes) * @property {double} `free-disk-space` Free recording disk space (in megabytes) */ obs_data_t* WSEvents::GetStats() { obs_data_t* stats = obs_data_create(); double cpuUsage = os_cpu_usage_info_query(cpuUsageInfo); double memoryUsage = (double)os_get_proc_resident_size() / (1024.0 * 1024.0); video_t* mainVideo = obs_get_video(); uint32_t outputTotalFrames = video_output_get_total_frames(mainVideo); uint32_t outputSkippedFrames = video_output_get_skipped_frames(mainVideo); double averageFrameTime = (double)obs_get_average_frame_time_ns() / 1000000.0; config_t* currentProfile = obs_frontend_get_profile_config(); const char* outputMode = config_get_string(currentProfile, "Output", "Mode"); const char* path = strcmp(outputMode, "Advanced") ? config_get_string(currentProfile, "SimpleOutput", "FilePath") : config_get_string(currentProfile, "AdvOut", "RecFilePath"); double freeDiskSpace = (double)os_get_free_disk_space(path) / (1024.0 * 1024.0); obs_data_set_double(stats, "fps", obs_get_active_fps()); obs_data_set_int(stats, "render-total-frames", obs_get_total_frames()); obs_data_set_int(stats, "render-missed-frames", obs_get_lagged_frames()); obs_data_set_int(stats, "output-total-frames", outputTotalFrames); obs_data_set_int(stats, "output-skipped-frames", outputSkippedFrames); obs_data_set_double(stats, "average-frame-time", averageFrameTime); obs_data_set_double(stats, "cpu-usage", cpuUsage); obs_data_set_double(stats, "memory-usage", memoryUsage); obs_data_set_double(stats, "free-disk-space", freeDiskSpace); return stats; }