2024-12-11 17:43:35 +01:00
|
|
|
#include "BasicOutputHandler.hpp"
|
|
|
|
|
#include "AdvancedOutput.hpp"
|
|
|
|
|
#include "SimpleOutput.hpp"
|
|
|
|
|
|
|
|
|
|
#include <utility/MultitrackVideoError.hpp>
|
|
|
|
|
#include <utility/StartMultiTrackVideoStreamingGuard.hpp>
|
|
|
|
|
#include <utility/VCamConfig.hpp>
|
|
|
|
|
#include <widgets/OBSBasic.hpp>
|
|
|
|
|
|
2024-01-17 08:24:40 +01:00
|
|
|
#include <qt-wrappers.hpp>
|
2024-12-11 17:43:35 +01:00
|
|
|
|
|
|
|
|
#include <QThreadPool>
|
2015-02-06 03:17:33 -08:00
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
2019-02-05 18:22:40 -08:00
|
|
|
extern bool EncoderAvailable(const char *encoder);
|
|
|
|
|
|
2019-02-26 06:37:01 -08:00
|
|
|
volatile bool streaming_active = false;
|
|
|
|
|
volatile bool recording_active = false;
|
2019-07-07 15:47:29 -07:00
|
|
|
volatile bool recording_paused = false;
|
2019-02-26 06:37:01 -08:00
|
|
|
volatile bool replaybuf_active = false;
|
2020-06-20 07:42:14 -07:00
|
|
|
volatile bool virtualcam_active = false;
|
2019-02-26 06:37:01 -08:00
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStreamStarting(void *data, calldata_t *params)
|
2015-09-06 16:19:53 -07:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output");
|
|
|
|
|
|
|
|
|
|
int sec = (int)obs_output_get_active_delay(obj);
|
|
|
|
|
if (sec == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
output->delayActive = true;
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec));
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStreamStopping(void *data, calldata_t *params)
|
2015-09-06 16:19:53 -07:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output");
|
|
|
|
|
|
|
|
|
|
int sec = (int)obs_output_get_active_delay(obj);
|
|
|
|
|
if (sec == 0)
|
2016-06-22 01:47:08 -07:00
|
|
|
QMetaObject::invokeMethod(output->main, "StreamStopping");
|
|
|
|
|
else
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec));
|
2015-09-06 16:19:53 -07:00
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStartStreaming(void *data, calldata_t * /* params */)
|
2015-02-06 03:17:33 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
2015-09-06 16:07:45 -07:00
|
|
|
output->streamingActive = true;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&streaming_active, true);
|
2015-02-06 03:17:33 -08:00
|
|
|
QMetaObject::invokeMethod(output->main, "StreamingStart");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStopStreaming(void *data, calldata_t *params)
|
2015-02-06 03:17:33 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
int code = (int)calldata_int(params, "code");
|
2017-05-15 12:03:00 +02:00
|
|
|
const char *last_error = calldata_string(params, "last_error");
|
|
|
|
|
|
|
|
|
|
QString arg_last_error = QString::fromUtf8(last_error);
|
2015-02-06 03:17:33 -08:00
|
|
|
|
2015-09-06 16:07:45 -07:00
|
|
|
output->streamingActive = false;
|
2015-09-06 16:19:53 -07:00
|
|
|
output->delayActive = false;
|
2024-04-18 15:41:36 +02:00
|
|
|
output->multitrackVideoActive = false;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&streaming_active, false);
|
2015-02-06 03:17:33 -08:00
|
|
|
QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error));
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStartRecording(void *data, calldata_t * /* params */)
|
2015-02-06 03:17:33 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
|
2015-09-06 16:07:45 -07:00
|
|
|
output->recordingActive = true;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&recording_active, true);
|
2015-02-06 03:17:33 -08:00
|
|
|
QMetaObject::invokeMethod(output->main, "RecordingStart");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStopRecording(void *data, calldata_t *params)
|
2015-02-06 03:17:33 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
2015-06-09 21:00:22 -07:00
|
|
|
int code = (int)calldata_int(params, "code");
|
2019-03-30 15:44:44 +01:00
|
|
|
const char *last_error = calldata_string(params, "last_error");
|
|
|
|
|
|
|
|
|
|
QString arg_last_error = QString::fromUtf8(last_error);
|
2015-02-06 03:17:33 -08:00
|
|
|
|
2015-09-06 16:07:45 -07:00
|
|
|
output->recordingActive = false;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&recording_active, false);
|
2019-07-07 15:47:29 -07:00
|
|
|
os_atomic_set_bool(&recording_paused, false);
|
2015-06-09 21:00:22 -07:00
|
|
|
QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error));
|
2015-02-06 03:17:33 -08:00
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSRecordStopping(void *data, calldata_t * /* params */)
|
2016-06-22 01:47:08 -07:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "RecordStopping");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSRecordFileChanged(void *data, calldata_t *params)
|
2021-10-03 19:06:46 +09:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
const char *next_file = calldata_string(params, "next_file");
|
|
|
|
|
|
|
|
|
|
QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str());
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file));
|
|
|
|
|
|
|
|
|
|
output->lastRecordingPath = next_file;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStartReplayBuffer(void *data, calldata_t * /* params */)
|
2016-12-09 14:40:04 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
|
|
|
|
|
output->replayBufferActive = true;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&replaybuf_active, true);
|
2016-12-09 14:40:04 -08:00
|
|
|
QMetaObject::invokeMethod(output->main, "ReplayBufferStart");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSStopReplayBuffer(void *data, calldata_t *params)
|
2016-12-09 14:40:04 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
int code = (int)calldata_int(params, "code");
|
|
|
|
|
|
|
|
|
|
output->replayBufferActive = false;
|
2019-02-26 06:37:01 -08:00
|
|
|
os_atomic_set_bool(&replaybuf_active, false);
|
2016-12-09 14:40:04 -08:00
|
|
|
QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code));
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSReplayBufferStopping(void *data, calldata_t * /* params */)
|
2016-12-09 14:40:04 -08:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "ReplayBufferStopping");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void OBSReplayBufferSaved(void *data, calldata_t * /* params */)
|
2020-10-12 16:55:01 -07:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-28 20:42:41 -06:00
|
|
|
static void OBSStartVirtualCam(void *data, calldata_t * /* params */)
|
2020-06-20 07:42:14 -07:00
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
|
|
|
|
|
output->virtualCamActive = true;
|
|
|
|
|
os_atomic_set_bool(&virtualcam_active, true);
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "OnVirtualCamStart");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void OBSStopVirtualCam(void *data, calldata_t *params)
|
|
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
|
|
|
|
int code = (int)calldata_int(params, "code");
|
|
|
|
|
|
|
|
|
|
output->virtualCamActive = false;
|
|
|
|
|
os_atomic_set_bool(&virtualcam_active, false);
|
|
|
|
|
QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code));
|
UI: Wait for full vcam deactivation to destroy its view
A bug which was technically introduced in df446c3f6e: the author
assigned video mixes to the virtual camera and did not realize you have
to wait for full capture stop with the "deactivate" signal rather than
the "stop" signal. This situation is understandable because these
signals are likely confusing and need more documentation.
The way an output works when it stops is it has three stages:
- "stopping", which is the moment the user themselves stop the output
- "stop", when the output's implementation has stopped and returns
success or an error code
- "deactivate", when the output inself (not the implementation) has
fully stopped capturing encoded or raw frame data after the
implementation has stopped. This is done in a separate thread,
end_data_capture_thread(), for performance and data race reasons, and
is when it's "actually fully stopped for real this time"
(Lain note: I sincerely apologize for this confusing signal design)
The author of df446c3f6e was likely confused as to why they could not
destroy the video mix in the "stop" signal without triggering a race
condition in end_data_capture_thread(), and instead decided to make a
workaround to clear the video mix set to the output before destroying
the video mix. Unsetting the video mix I'm guessing *seemed* to fix the
problem for them at the time, and destroying the video mix separately
after that automatically stops capture, so it *technically* worked
because the deactivate thread cannot be called until the
implementation's stop signal has executed. However, it was still the
incorrect way to handle the problem, because it circumvents the output's
mechanism for deactivating the frame data capture by destroying the view
instead.
The reason this was the incorrect way to handle the problem became
exposed in 7cd7ca80f8, when tytan made it so it uses the main video mix
under certain circumstances. The main view is special and cannot be
destroyed, so the mechanism to stop frame data capture that the
virtualcam was using before failed, and raw frame data capture continued
perpetually until the end of the program erroneously.
This fix solves the bug by using the "deactivate" signal rather than the
"stop" signal, thus allowing the output to fully end its data capture
and *then* destroy the video mix.
Fixes obsproject/obs-studio#9153
2023-07-06 14:56:48 -07:00
|
|
|
}
|
2022-07-19 09:32:39 -07:00
|
|
|
|
UI: Wait for full vcam deactivation to destroy its view
A bug which was technically introduced in df446c3f6e: the author
assigned video mixes to the virtual camera and did not realize you have
to wait for full capture stop with the "deactivate" signal rather than
the "stop" signal. This situation is understandable because these
signals are likely confusing and need more documentation.
The way an output works when it stops is it has three stages:
- "stopping", which is the moment the user themselves stop the output
- "stop", when the output's implementation has stopped and returns
success or an error code
- "deactivate", when the output inself (not the implementation) has
fully stopped capturing encoded or raw frame data after the
implementation has stopped. This is done in a separate thread,
end_data_capture_thread(), for performance and data race reasons, and
is when it's "actually fully stopped for real this time"
(Lain note: I sincerely apologize for this confusing signal design)
The author of df446c3f6e was likely confused as to why they could not
destroy the video mix in the "stop" signal without triggering a race
condition in end_data_capture_thread(), and instead decided to make a
workaround to clear the video mix set to the output before destroying
the video mix. Unsetting the video mix I'm guessing *seemed* to fix the
problem for them at the time, and destroying the video mix separately
after that automatically stops capture, so it *technically* worked
because the deactivate thread cannot be called until the
implementation's stop signal has executed. However, it was still the
incorrect way to handle the problem, because it circumvents the output's
mechanism for deactivating the frame data capture by destroying the view
instead.
The reason this was the incorrect way to handle the problem became
exposed in 7cd7ca80f8, when tytan made it so it uses the main video mix
under certain circumstances. The main view is special and cannot be
destroyed, so the mechanism to stop frame data capture that the
virtualcam was using before failed, and raw frame data capture continued
perpetually until the end of the program erroneously.
This fix solves the bug by using the "deactivate" signal rather than the
"stop" signal, thus allowing the output to fully end its data capture
and *then* destroy the video mix.
Fixes obsproject/obs-studio#9153
2023-07-06 14:56:48 -07:00
|
|
|
static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */)
|
|
|
|
|
{
|
|
|
|
|
BasicOutputHandler *output = static_cast<BasicOutputHandler *>(data);
|
2022-08-22 16:04:13 +02:00
|
|
|
output->DestroyVirtualCamView();
|
2020-06-20 07:42:14 -07:00
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
bool return_first_id(void *data, const char *id)
|
2022-10-07 11:42:12 +02:00
|
|
|
{
|
|
|
|
|
const char **output = (const char **)data;
|
|
|
|
|
|
|
|
|
|
*output = id;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
const char *GetStreamOutputType(const obs_service_t *service)
|
2022-10-07 11:42:12 +02:00
|
|
|
{
|
|
|
|
|
const char *protocol = obs_service_get_protocol(service);
|
|
|
|
|
const char *output = nullptr;
|
|
|
|
|
|
|
|
|
|
if (!protocol) {
|
|
|
|
|
blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service));
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!obs_is_output_protocol_registered(protocol)) {
|
|
|
|
|
blog(LOG_WARNING, "The protocol '%s' is not registered", protocol);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Check if the service has a preferred output type */
|
|
|
|
|
output = obs_service_get_preferred_output_type(service);
|
|
|
|
|
if (output) {
|
|
|
|
|
if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0)
|
|
|
|
|
return output;
|
|
|
|
|
|
|
|
|
|
blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Otherwise, prefer first-party output types */
|
|
|
|
|
if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) {
|
|
|
|
|
return "rtmp_output";
|
|
|
|
|
} else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) {
|
|
|
|
|
return "ffmpeg_hls_muxer";
|
|
|
|
|
} else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) {
|
|
|
|
|
return "ffmpeg_mpegts_muxer";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* If third-party protocol, use the first enumerated type */
|
|
|
|
|
obs_enum_output_types_with_protocol(protocol, &output, return_first_id);
|
|
|
|
|
if (output)
|
|
|
|
|
return output;
|
|
|
|
|
|
|
|
|
|
blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service));
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_)
|
2020-06-20 07:42:14 -07:00
|
|
|
{
|
|
|
|
|
if (main->vcamEnabled) {
|
2022-09-20 15:45:27 +02:00
|
|
|
virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr);
|
2020-06-20 07:42:14 -07:00
|
|
|
|
|
|
|
|
signal_handler_t *signal = obs_output_get_signal_handler(virtualCam);
|
|
|
|
|
startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this);
|
|
|
|
|
stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this);
|
UI: Wait for full vcam deactivation to destroy its view
A bug which was technically introduced in df446c3f6e: the author
assigned video mixes to the virtual camera and did not realize you have
to wait for full capture stop with the "deactivate" signal rather than
the "stop" signal. This situation is understandable because these
signals are likely confusing and need more documentation.
The way an output works when it stops is it has three stages:
- "stopping", which is the moment the user themselves stop the output
- "stop", when the output's implementation has stopped and returns
success or an error code
- "deactivate", when the output inself (not the implementation) has
fully stopped capturing encoded or raw frame data after the
implementation has stopped. This is done in a separate thread,
end_data_capture_thread(), for performance and data race reasons, and
is when it's "actually fully stopped for real this time"
(Lain note: I sincerely apologize for this confusing signal design)
The author of df446c3f6e was likely confused as to why they could not
destroy the video mix in the "stop" signal without triggering a race
condition in end_data_capture_thread(), and instead decided to make a
workaround to clear the video mix set to the output before destroying
the video mix. Unsetting the video mix I'm guessing *seemed* to fix the
problem for them at the time, and destroying the video mix separately
after that automatically stops capture, so it *technically* worked
because the deactivate thread cannot be called until the
implementation's stop signal has executed. However, it was still the
incorrect way to handle the problem, because it circumvents the output's
mechanism for deactivating the frame data capture by destroying the view
instead.
The reason this was the incorrect way to handle the problem became
exposed in 7cd7ca80f8, when tytan made it so it uses the main video mix
under certain circumstances. The main view is special and cannot be
destroyed, so the mechanism to stop frame data capture that the
virtualcam was using before failed, and raw frame data capture continued
perpetually until the end of the program erroneously.
This fix solves the bug by using the "deactivate" signal rather than the
"stop" signal, thus allowing the output to fully end its data capture
and *then* destroy the video mix.
Fixes obsproject/obs-studio#9153
2023-07-06 14:56:48 -07:00
|
|
|
deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this);
|
2020-06-20 07:42:14 -07:00
|
|
|
}
|
2024-04-18 15:41:36 +02:00
|
|
|
|
|
|
|
|
auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo");
|
|
|
|
|
if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) {
|
|
|
|
|
auto service = main_->GetService();
|
|
|
|
|
OBSDataAutoRelease settings = obs_service_get_settings(service);
|
|
|
|
|
multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url");
|
|
|
|
|
}
|
|
|
|
|
if (multitrack_enabled)
|
|
|
|
|
multitrackVideo = make_unique<MultitrackVideoOutput>();
|
2020-06-20 07:42:14 -07:00
|
|
|
}
|
|
|
|
|
|
2023-11-07 11:58:52 -05:00
|
|
|
extern void log_vcam_changed(const VCamConfig &config, bool starting);
|
2023-11-04 04:11:03 +01:00
|
|
|
|
2020-06-20 07:42:14 -07:00
|
|
|
bool BasicOutputHandler::StartVirtualCam()
|
|
|
|
|
{
|
2022-08-22 16:04:13 +02:00
|
|
|
if (!main->vcamEnabled)
|
|
|
|
|
return false;
|
2022-07-19 09:32:39 -07:00
|
|
|
|
2022-11-15 10:00:41 +01:00
|
|
|
bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView;
|
|
|
|
|
|
|
|
|
|
if (!virtualCamView && !typeIsProgram)
|
2022-08-22 16:04:13 +02:00
|
|
|
virtualCamView = obs_view_create();
|
2020-09-03 06:56:03 -07:00
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
UpdateVirtualCamOutputSource();
|
2022-08-11 14:28:27 +09:00
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
if (!virtualCamVideo) {
|
2022-11-15 10:00:41 +01:00
|
|
|
virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView);
|
2022-08-11 14:28:27 +09:00
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
if (!virtualCamVideo)
|
|
|
|
|
return false;
|
2020-06-20 07:42:14 -07:00
|
|
|
}
|
2022-08-22 16:04:13 +02:00
|
|
|
|
|
|
|
|
obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio());
|
|
|
|
|
if (!Active())
|
|
|
|
|
SetupOutputs();
|
|
|
|
|
|
|
|
|
|
bool success = obs_output_start(virtualCam);
|
2023-02-26 23:49:46 +01:00
|
|
|
if (!success) {
|
|
|
|
|
QString errorReason;
|
|
|
|
|
|
|
|
|
|
const char *error = obs_output_get_last_error(virtualCam);
|
|
|
|
|
if (error) {
|
|
|
|
|
errorReason = QT_UTF8(error);
|
|
|
|
|
} else {
|
|
|
|
|
errorReason = QTStr("Output.StartFailedGeneric");
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-19 11:18:00 +09:00
|
|
|
QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason);
|
2023-02-26 23:49:46 +01:00
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
DestroyVirtualCamView();
|
2023-02-26 23:49:46 +01:00
|
|
|
}
|
2022-08-22 16:04:13 +02:00
|
|
|
|
2023-11-07 11:58:52 -05:00
|
|
|
log_vcam_changed(main->vcamConfig, true);
|
2023-11-04 04:11:03 +01:00
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
return success;
|
2020-06-20 07:42:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BasicOutputHandler::StopVirtualCam()
|
|
|
|
|
{
|
|
|
|
|
if (main->vcamEnabled) {
|
|
|
|
|
obs_output_stop(virtualCam);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BasicOutputHandler::VirtualCamActive() const
|
|
|
|
|
{
|
|
|
|
|
if (main->vcamEnabled) {
|
|
|
|
|
return obs_output_active(virtualCam);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
void BasicOutputHandler::UpdateVirtualCamOutputSource()
|
|
|
|
|
{
|
|
|
|
|
if (!main->vcamEnabled || !virtualCamView)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
OBSSourceAutoRelease source;
|
|
|
|
|
|
|
|
|
|
switch (main->vcamConfig.type) {
|
2022-11-15 10:00:41 +01:00
|
|
|
case VCamOutputType::Invalid:
|
|
|
|
|
case VCamOutputType::ProgramView:
|
2023-05-13 17:19:00 +02:00
|
|
|
DestroyVirtualCameraScene();
|
2022-11-15 10:00:41 +01:00
|
|
|
return;
|
|
|
|
|
case VCamOutputType::PreviewOutput: {
|
|
|
|
|
DestroyVirtualCameraScene();
|
|
|
|
|
OBSSource s = main->GetCurrentSceneSource();
|
|
|
|
|
obs_source_get_ref(s);
|
|
|
|
|
source = s.Get();
|
2022-08-22 16:04:13 +02:00
|
|
|
break;
|
2022-11-15 10:00:41 +01:00
|
|
|
}
|
2022-08-22 16:04:13 +02:00
|
|
|
case VCamOutputType::SceneOutput:
|
2023-05-13 17:19:00 +02:00
|
|
|
DestroyVirtualCameraScene();
|
2022-08-22 16:04:13 +02:00
|
|
|
source = obs_get_source_by_name(main->vcamConfig.scene.c_str());
|
|
|
|
|
break;
|
|
|
|
|
case VCamOutputType::SourceOutput:
|
|
|
|
|
OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str());
|
|
|
|
|
|
|
|
|
|
if (!vCamSourceScene)
|
|
|
|
|
vCamSourceScene = obs_scene_create_private("vcam_source");
|
|
|
|
|
source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene));
|
|
|
|
|
|
|
|
|
|
if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) {
|
|
|
|
|
obs_sceneitem_remove(vCamSourceSceneItem);
|
|
|
|
|
vCamSourceSceneItem = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!vCamSourceSceneItem) {
|
|
|
|
|
vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s);
|
|
|
|
|
|
|
|
|
|
obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER);
|
|
|
|
|
obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER);
|
|
|
|
|
|
|
|
|
|
const struct vec2 size = {
|
|
|
|
|
(float)obs_source_get_width(source),
|
|
|
|
|
(float)obs_source_get_height(source),
|
|
|
|
|
};
|
|
|
|
|
obs_sceneitem_set_bounds(vCamSourceSceneItem, &size);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0);
|
|
|
|
|
if (source != current)
|
|
|
|
|
obs_view_set_source(virtualCamView, 0, source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BasicOutputHandler::DestroyVirtualCamView()
|
|
|
|
|
{
|
2022-11-15 10:00:41 +01:00
|
|
|
if (main->vcamConfig.type == VCamOutputType::ProgramView) {
|
|
|
|
|
virtualCamVideo = nullptr;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-22 16:04:13 +02:00
|
|
|
obs_view_remove(virtualCamView);
|
|
|
|
|
obs_view_set_source(virtualCamView, 0, nullptr);
|
|
|
|
|
virtualCamVideo = nullptr;
|
|
|
|
|
|
|
|
|
|
obs_view_destroy(virtualCamView);
|
|
|
|
|
virtualCamView = nullptr;
|
|
|
|
|
|
2023-05-13 17:19:00 +02:00
|
|
|
DestroyVirtualCameraScene();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BasicOutputHandler::DestroyVirtualCameraScene()
|
|
|
|
|
{
|
2022-08-22 16:04:13 +02:00
|
|
|
if (!vCamSourceScene)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
obs_scene_release(vCamSourceScene);
|
|
|
|
|
vCamSourceScene = nullptr;
|
|
|
|
|
vCamSourceSceneItem = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-13 03:57:42 -07:00
|
|
|
const char *FindAudioEncoderFromCodec(const char *type)
|
|
|
|
|
{
|
|
|
|
|
const char *alt_enc_id = nullptr;
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
|
|
|
|
|
while (obs_enum_encoder_types(i++, &alt_enc_id)) {
|
|
|
|
|
const char *codec = obs_get_encoder_codec(alt_enc_id);
|
|
|
|
|
if (strcmp(type, codec) == 0) {
|
|
|
|
|
return alt_enc_id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 17:43:35 +01:00
|
|
|
void clear_archive_encoder(obs_output_t *output, const char *expected_name)
|
2020-12-03 00:59:43 -08:00
|
|
|
{
|
|
|
|
|
obs_encoder_t *last = obs_output_get_audio_encoder(output, 1);
|
|
|
|
|
bool clear = false;
|
|
|
|
|
|
|
|
|
|
/* ensures that we don't remove twitch's soundtrack encoder */
|
|
|
|
|
if (last) {
|
|
|
|
|
const char *name = obs_encoder_get_name(last);
|
|
|
|
|
clear = name && strcmp(name, expected_name) == 0;
|
|
|
|
|
obs_encoder_release(last);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clear)
|
|
|
|
|
obs_output_set_audio_encoder(output, nullptr, 1);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 15:48:55 +02:00
|
|
|
void BasicOutputHandler::SetupAutoRemux(const char *&container)
|
2020-09-26 08:19:27 -07:00
|
|
|
{
|
|
|
|
|
bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux");
|
2023-04-03 15:48:55 +02:00
|
|
|
if (autoRemux && strcmp(container, "mp4") == 0)
|
|
|
|
|
container = "mkv";
|
2020-09-26 08:19:27 -07:00
|
|
|
}
|
|
|
|
|
|
2023-04-03 15:48:55 +02:00
|
|
|
std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace,
|
|
|
|
|
bool overwrite, const char *format, bool ffmpeg)
|
2020-09-26 08:19:27 -07:00
|
|
|
{
|
2021-02-04 06:01:35 -06:00
|
|
|
if (!ffmpeg)
|
2023-04-03 15:48:55 +02:00
|
|
|
SetupAutoRemux(container);
|
2021-02-04 06:01:35 -06:00
|
|
|
|
2023-04-03 15:48:55 +02:00
|
|
|
string dst = GetOutputFilename(path, container, noSpace, overwrite, format);
|
2021-02-04 06:01:35 -06:00
|
|
|
lastRecordingPath = dst;
|
2020-09-26 08:19:27 -07:00
|
|
|
return dst;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-18 15:41:36 +02:00
|
|
|
extern std::string DeserializeConfigText(const char *text);
|
|
|
|
|
|
2024-07-08 14:07:10 +02:00
|
|
|
std::shared_future<void> BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id,
|
|
|
|
|
size_t main_audio_mixer,
|
|
|
|
|
std::optional<size_t> vod_track_mixer,
|
|
|
|
|
std::function<void(std::optional<bool>)> continuation)
|
2024-04-18 15:41:36 +02:00
|
|
|
{
|
2024-07-08 14:07:10 +02:00
|
|
|
auto start_streaming_guard = std::make_shared<StartMultitrackVideoStreamingGuard>();
|
|
|
|
|
if (!multitrackVideo) {
|
|
|
|
|
continuation(std::nullopt);
|
|
|
|
|
return start_streaming_guard->GetFuture();
|
|
|
|
|
}
|
2024-04-18 15:41:36 +02:00
|
|
|
|
|
|
|
|
multitrackVideoActive = false;
|
|
|
|
|
|
|
|
|
|
streamDelayStarting.Disconnect();
|
|
|
|
|
streamStopping.Disconnect();
|
|
|
|
|
startStreaming.Disconnect();
|
|
|
|
|
stopStreaming.Disconnect();
|
|
|
|
|
|
|
|
|
|
bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0;
|
|
|
|
|
|
|
|
|
|
std::optional<std::string> custom_config = std::nullopt;
|
|
|
|
|
if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled"))
|
|
|
|
|
custom_config = DeserializeConfigText(
|
|
|
|
|
config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride"));
|
|
|
|
|
|
2025-02-05 13:32:03 +01:00
|
|
|
std::optional<QString> extraCanvasUUID;
|
|
|
|
|
const char *uuid = config_get_string(main->Config(), "Stream1", "MultitrackExtraCanvas");
|
|
|
|
|
if (uuid && *uuid) {
|
|
|
|
|
extraCanvasUUID = uuid;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-18 15:41:36 +02:00
|
|
|
OBSDataAutoRelease settings = obs_service_get_settings(service);
|
|
|
|
|
QString key = obs_data_get_string(settings, "key");
|
|
|
|
|
|
|
|
|
|
const char *service_name = "<unknown>";
|
|
|
|
|
if (is_custom && obs_data_has_user_value(settings, "service_name")) {
|
|
|
|
|
service_name = obs_data_get_string(settings, "service_name");
|
|
|
|
|
} else if (!is_custom) {
|
|
|
|
|
service_name = obs_data_get_string(settings, "service");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<std::string> custom_rtmp_url;
|
2024-06-05 17:48:25 +02:00
|
|
|
std::optional<bool> use_rtmps;
|
2024-04-18 15:41:36 +02:00
|
|
|
auto server = obs_data_get_string(settings, "server");
|
2024-06-05 16:52:15 +02:00
|
|
|
if (strncmp(server, "auto", 4) != 0) {
|
2024-04-18 15:41:36 +02:00
|
|
|
custom_rtmp_url = server;
|
2024-06-05 17:48:25 +02:00
|
|
|
} else {
|
|
|
|
|
QString server_ = server;
|
|
|
|
|
use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive);
|
2024-04-18 15:41:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto service_custom_server = obs_data_get_bool(settings, "using_custom_server");
|
|
|
|
|
if (custom_rtmp_url.has_value()) {
|
|
|
|
|
blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto maximum_aggregate_bitrate =
|
|
|
|
|
config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto")
|
|
|
|
|
? std::nullopt
|
|
|
|
|
: std::make_optional<uint32_t>(
|
|
|
|
|
config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate"));
|
|
|
|
|
|
|
|
|
|
auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto")
|
|
|
|
|
? std::nullopt
|
|
|
|
|
: std::make_optional<uint32_t>(config_get_int(
|
|
|
|
|
main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks"));
|
|
|
|
|
|
|
|
|
|
auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig();
|
|
|
|
|
|
2024-07-08 14:07:10 +02:00
|
|
|
auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service},
|
|
|
|
|
continuation =
|
|
|
|
|
std::move(continuation)](std::optional<MultitrackVideoError> error) {
|
2024-07-09 13:25:49 +02:00
|
|
|
if (error) {
|
|
|
|
|
OBSDataAutoRelease service_settings = obs_service_get_settings(service);
|
|
|
|
|
auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
|
|
|
|
|
if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
|
|
|
|
|
multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
multitrackVideoActive = false;
|
|
|
|
|
if (!error->ShowDialog(main, multitrack_video_name))
|
2024-07-08 14:07:10 +02:00
|
|
|
return continuation(false);
|
|
|
|
|
return continuation(std::nullopt);
|
2024-07-09 13:25:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
multitrackVideoActive = true;
|
|
|
|
|
|
|
|
|
|
auto signal_handler = multitrackVideo->StreamingSignalHandler();
|
|
|
|
|
|
|
|
|
|
streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this);
|
|
|
|
|
streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this);
|
|
|
|
|
|
|
|
|
|
startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this);
|
|
|
|
|
stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this);
|
2024-07-08 14:07:10 +02:00
|
|
|
return continuation(true);
|
2024-07-09 13:25:49 +02:00
|
|
|
};
|
|
|
|
|
|
2024-04-18 15:41:36 +02:00
|
|
|
QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(),
|
|
|
|
|
service_name = std::string{service_name}, service = OBSService{service},
|
2024-07-08 14:07:10 +02:00
|
|
|
stream_dump_config = OBSData{stream_dump_config},
|
|
|
|
|
start_streaming_guard = start_streaming_guard]() mutable {
|
|
|
|
|
std::optional<MultitrackVideoError> error;
|
2024-04-18 15:41:36 +02:00
|
|
|
try {
|
|
|
|
|
multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key,
|
|
|
|
|
audio_encoder_id.c_str(), maximum_aggregate_bitrate,
|
|
|
|
|
maximum_video_tracks, custom_config, stream_dump_config,
|
2025-02-05 13:32:03 +01:00
|
|
|
main_audio_mixer, vod_track_mixer, use_rtmps,
|
|
|
|
|
extraCanvasUUID);
|
2024-07-08 14:07:10 +02:00
|
|
|
} catch (const MultitrackVideoError &error_) {
|
|
|
|
|
error.emplace(error_);
|
2024-04-18 15:41:36 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-08 14:07:10 +02:00
|
|
|
QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); });
|
|
|
|
|
});
|
2024-04-18 15:41:36 +02:00
|
|
|
|
2024-07-08 14:07:10 +02:00
|
|
|
return start_streaming_guard->GetFuture();
|
2024-04-18 15:41:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig()
|
|
|
|
|
{
|
|
|
|
|
auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled");
|
|
|
|
|
|
|
|
|
|
if (!stream_dump_enabled)
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath");
|
|
|
|
|
bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace");
|
|
|
|
|
const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting");
|
|
|
|
|
bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists");
|
2024-06-07 18:46:19 +02:00
|
|
|
bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4");
|
2024-04-18 15:41:36 +02:00
|
|
|
|
|
|
|
|
string f;
|
|
|
|
|
|
|
|
|
|
OBSDataAutoRelease settings = obs_data_create();
|
|
|
|
|
f = GetFormatString(filenameFormat, nullptr, nullptr);
|
2024-06-07 18:46:19 +02:00
|
|
|
string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(),
|
2024-04-18 15:41:36 +02:00
|
|
|
// never remux stream dump
|
|
|
|
|
false);
|
|
|
|
|
obs_data_set_string(settings, "path", strPath.c_str());
|
2024-06-07 18:46:19 +02:00
|
|
|
|
|
|
|
|
if (useMP4) {
|
|
|
|
|
obs_data_set_bool(settings, "use_mp4", true);
|
|
|
|
|
obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1");
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-18 15:41:36 +02:00
|
|
|
return settings;
|
|
|
|
|
}
|
|
|
|
|
|
2015-02-06 03:17:33 -08:00
|
|
|
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main)
|
|
|
|
|
{
|
|
|
|
|
return new SimpleOutput(main);
|
|
|
|
|
}
|
2015-01-26 13:41:22 -08:00
|
|
|
|
|
|
|
|
BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main)
|
|
|
|
|
{
|
|
|
|
|
return new AdvancedOutput(main);
|
|
|
|
|
}
|