|
@@ -0,0 +1,950 @@
|
|
|
+#include "multitrack-video-output.hpp"
|
|
|
+
|
|
|
+#include <util/dstr.hpp>
|
|
|
+#include <util/platform.h>
|
|
|
+#include <util/profiler.hpp>
|
|
|
+#include <util/util.hpp>
|
|
|
+#include <obs-frontend-api.h>
|
|
|
+#include <obs-app.hpp>
|
|
|
+#include <obs.hpp>
|
|
|
+#include <remote-text.hpp>
|
|
|
+
|
|
|
+#include <algorithm>
|
|
|
+#include <cinttypes>
|
|
|
+#include <cmath>
|
|
|
+#include <numeric>
|
|
|
+#include <optional>
|
|
|
+#include <string>
|
|
|
+#include <vector>
|
|
|
+
|
|
|
+#include <QAbstractButton>
|
|
|
+#include <QMessageBox>
|
|
|
+#include <QObject>
|
|
|
+#include <QPushButton>
|
|
|
+#include <QScopeGuard>
|
|
|
+#include <QString>
|
|
|
+#include <QThreadPool>
|
|
|
+#include <QUrl>
|
|
|
+#include <QUrlQuery>
|
|
|
+#include <QUuid>
|
|
|
+
|
|
|
+#include <nlohmann/json.hpp>
|
|
|
+
|
|
|
+#include "system-info.hpp"
|
|
|
+#include "goliveapi-postdata.hpp"
|
|
|
+#include "goliveapi-network.hpp"
|
|
|
+#include "multitrack-video-error.hpp"
|
|
|
+#include "qt-helpers.hpp"
|
|
|
+#include "models/multitrack-video.hpp"
|
|
|
+
|
|
|
+Qt::ConnectionType BlockingConnectionTypeFor(QObject *object)
|
|
|
+{
|
|
|
+ return object->thread() == QThread::currentThread()
|
|
|
+ ? Qt::DirectConnection
|
|
|
+ : Qt::BlockingQueuedConnection;
|
|
|
+}
|
|
|
+
|
|
|
+bool MultitrackVideoDeveloperModeEnabled()
|
|
|
+{
|
|
|
+ static bool developer_mode = [] {
|
|
|
+ auto args = qApp->arguments();
|
|
|
+ for (const auto &arg : args) {
|
|
|
+ if (arg == "--enable-multitrack-video-dev") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }();
|
|
|
+ return developer_mode;
|
|
|
+}
|
|
|
+
|
|
|
+static OBSServiceAutoRelease
|
|
|
+create_service(const GoLiveApi::Config &go_live_config,
|
|
|
+ const std::optional<std::string> &rtmp_url,
|
|
|
+ const QString &in_stream_key)
|
|
|
+{
|
|
|
+ const char *url = nullptr;
|
|
|
+ QString stream_key = in_stream_key;
|
|
|
+
|
|
|
+ const auto &ingest_endpoints = go_live_config.ingest_endpoints;
|
|
|
+
|
|
|
+ for (auto &endpoint : ingest_endpoints) {
|
|
|
+ if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ url = endpoint.url_template.c_str();
|
|
|
+ if (endpoint.authentication &&
|
|
|
+ !endpoint.authentication->empty()) {
|
|
|
+ blog(LOG_INFO,
|
|
|
+ "Using stream key supplied by autoconfig");
|
|
|
+ stream_key = QString::fromStdString(
|
|
|
+ *endpoint.authentication);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rtmp_url.has_value()) {
|
|
|
+ // Despite being set by user, it was set to a ""
|
|
|
+ if (rtmp_url->empty()) {
|
|
|
+ throw MultitrackVideoError::warning(QTStr(
|
|
|
+ "FailedToStartStream.NoCustomRTMPURLInSettings"));
|
|
|
+ }
|
|
|
+
|
|
|
+ url = rtmp_url->c_str();
|
|
|
+ blog(LOG_INFO, "Using custom RTMP URL: '%s'", url);
|
|
|
+ } else {
|
|
|
+ if (!url) {
|
|
|
+ blog(LOG_ERROR, "No RTMP URL in go live config");
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.NoRTMPURLInConfig"));
|
|
|
+ }
|
|
|
+
|
|
|
+ blog(LOG_INFO, "Using URL template: '%s'", url);
|
|
|
+ }
|
|
|
+
|
|
|
+ DStr str;
|
|
|
+ dstr_cat(str, url);
|
|
|
+
|
|
|
+ // dstr_find does not protect against null, and dstr_cat will
|
|
|
+ // not initialize str if cat'ing with a null url
|
|
|
+ if (!dstr_is_empty(str)) {
|
|
|
+ auto found = dstr_find(str, "/{stream_key}");
|
|
|
+ if (found)
|
|
|
+ dstr_remove(str, found - str->array,
|
|
|
+ str->len - (found - str->array));
|
|
|
+ }
|
|
|
+
|
|
|
+ QUrl parsed_url{url};
|
|
|
+ QUrlQuery parsed_query{parsed_url};
|
|
|
+
|
|
|
+ if (!go_live_config.meta.config_id.empty()) {
|
|
|
+ parsed_query.addQueryItem(
|
|
|
+ "obsConfigId",
|
|
|
+ QString::fromStdString(go_live_config.meta.config_id));
|
|
|
+ }
|
|
|
+
|
|
|
+ auto key_with_param = stream_key;
|
|
|
+ if (!parsed_query.isEmpty())
|
|
|
+ key_with_param += "?" + parsed_query.toString();
|
|
|
+
|
|
|
+ OBSDataAutoRelease settings = obs_data_create();
|
|
|
+ obs_data_set_string(settings, "server", str->array);
|
|
|
+ obs_data_set_string(settings, "key",
|
|
|
+ key_with_param.toUtf8().constData());
|
|
|
+
|
|
|
+ auto service = obs_service_create(
|
|
|
+ "rtmp_custom", "multitrack video service", settings, nullptr);
|
|
|
+
|
|
|
+ if (!service) {
|
|
|
+ blog(LOG_WARNING, "Failed to create multitrack video service");
|
|
|
+ throw MultitrackVideoError::warning(QTStr(
|
|
|
+ "FailedToStartStream.FailedToCreateMultitrackVideoService"));
|
|
|
+ }
|
|
|
+
|
|
|
+ return service;
|
|
|
+}
|
|
|
+
|
|
|
+static void ensure_directory_exists(std::string &path)
|
|
|
+{
|
|
|
+ replace(path.begin(), path.end(), '\\', '/');
|
|
|
+
|
|
|
+ size_t last = path.rfind('/');
|
|
|
+ if (last == std::string::npos)
|
|
|
+ return;
|
|
|
+
|
|
|
+ std::string directory = path.substr(0, last);
|
|
|
+ os_mkdirs(directory.c_str());
|
|
|
+}
|
|
|
+
|
|
|
+std::string GetOutputFilename(const std::string &path, const char *format)
|
|
|
+{
|
|
|
+ std::string strPath;
|
|
|
+ strPath += path;
|
|
|
+
|
|
|
+ char lastChar = strPath.back();
|
|
|
+ if (lastChar != '/' && lastChar != '\\')
|
|
|
+ strPath += "/";
|
|
|
+
|
|
|
+ strPath += BPtr<char>{
|
|
|
+ os_generate_formatted_filename("flv", false, format)};
|
|
|
+ ensure_directory_exists(strPath);
|
|
|
+
|
|
|
+ return strPath;
|
|
|
+}
|
|
|
+
|
|
|
+static OBSOutputAutoRelease create_output()
|
|
|
+{
|
|
|
+ OBSOutputAutoRelease output = obs_output_create(
|
|
|
+ "rtmp_output", "rtmp multitrack video", nullptr, nullptr);
|
|
|
+
|
|
|
+ if (!output) {
|
|
|
+ blog(LOG_ERROR,
|
|
|
+ "Failed to create multitrack video rtmp output");
|
|
|
+ throw MultitrackVideoError::warning(QTStr(
|
|
|
+ "FailedToStartStream.FailedToCreateMultitrackVideoOutput"));
|
|
|
+ }
|
|
|
+
|
|
|
+ return output;
|
|
|
+}
|
|
|
+
|
|
|
+static OBSOutputAutoRelease create_recording_output(obs_data_t *settings)
|
|
|
+{
|
|
|
+ OBSOutputAutoRelease output = obs_output_create(
|
|
|
+ "flv_output", "flv multitrack video", settings, nullptr);
|
|
|
+
|
|
|
+ if (!output)
|
|
|
+ blog(LOG_ERROR, "Failed to create multitrack video flv output");
|
|
|
+
|
|
|
+ return output;
|
|
|
+}
|
|
|
+
|
|
|
+static void adjust_video_encoder_scaling(
|
|
|
+ const obs_video_info &ovi, obs_encoder_t *video_encoder,
|
|
|
+ const GoLiveApi::VideoEncoderConfiguration &encoder_config,
|
|
|
+ size_t encoder_index)
|
|
|
+{
|
|
|
+ auto requested_width = encoder_config.width;
|
|
|
+ auto requested_height = encoder_config.height;
|
|
|
+
|
|
|
+ if (ovi.output_width == requested_width ||
|
|
|
+ ovi.output_height == requested_height)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (ovi.base_width < requested_width ||
|
|
|
+ ovi.base_height < requested_height) {
|
|
|
+ blog(LOG_WARNING,
|
|
|
+ "Requested resolution exceeds canvas/available resolution for encoder %zu: %" PRIu32
|
|
|
+ "x%" PRIu32 " > %" PRIu32 "x%" PRIu32,
|
|
|
+ encoder_index, requested_width, requested_height,
|
|
|
+ ovi.base_width, ovi.base_height);
|
|
|
+ }
|
|
|
+
|
|
|
+ obs_encoder_set_scaled_size(video_encoder, requested_width,
|
|
|
+ requested_height);
|
|
|
+ obs_encoder_set_gpu_scale_type(
|
|
|
+ video_encoder,
|
|
|
+ encoder_config.gpu_scale_type.value_or(OBS_SCALE_BICUBIC));
|
|
|
+}
|
|
|
+
|
|
|
+static uint32_t closest_divisor(const obs_video_info &ovi,
|
|
|
+ const media_frames_per_second &target_fps)
|
|
|
+{
|
|
|
+ auto target = (uint64_t)target_fps.numerator * ovi.fps_den;
|
|
|
+ auto source = (uint64_t)ovi.fps_num * target_fps.denominator;
|
|
|
+ return std::max(1u, static_cast<uint32_t>(source / target));
|
|
|
+}
|
|
|
+
|
|
|
+static void adjust_encoder_frame_rate_divisor(
|
|
|
+ const obs_video_info &ovi, obs_encoder_t *video_encoder,
|
|
|
+ const GoLiveApi::VideoEncoderConfiguration &encoder_config,
|
|
|
+ const size_t encoder_index)
|
|
|
+{
|
|
|
+ if (!encoder_config.framerate) {
|
|
|
+ blog(LOG_WARNING, "`framerate` not specified for encoder %zu",
|
|
|
+ encoder_index);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ media_frames_per_second requested_fps = *encoder_config.framerate;
|
|
|
+
|
|
|
+ if (ovi.fps_num == requested_fps.numerator &&
|
|
|
+ ovi.fps_den == requested_fps.denominator)
|
|
|
+ return;
|
|
|
+
|
|
|
+ auto divisor = closest_divisor(ovi, requested_fps);
|
|
|
+ if (divisor <= 1)
|
|
|
+ return;
|
|
|
+
|
|
|
+ blog(LOG_INFO, "Setting frame rate divisor to %u for encoder %zu",
|
|
|
+ divisor, encoder_index);
|
|
|
+ obs_encoder_set_frame_rate_divisor(video_encoder, divisor);
|
|
|
+}
|
|
|
+
|
|
|
+static const std::vector<const char *> &get_available_encoders()
|
|
|
+{
|
|
|
+ // encoders are currently only registered during startup, so keeping
|
|
|
+ // a static vector around shouldn't be a problem
|
|
|
+ static std::vector<const char *> available_encoders = [] {
|
|
|
+ std::vector<const char *> available_encoders;
|
|
|
+ for (size_t i = 0;; i++) {
|
|
|
+ const char *id = nullptr;
|
|
|
+ if (!obs_enum_encoder_types(i, &id))
|
|
|
+ break;
|
|
|
+ available_encoders.push_back(id);
|
|
|
+ }
|
|
|
+ return available_encoders;
|
|
|
+ }();
|
|
|
+ return available_encoders;
|
|
|
+}
|
|
|
+
|
|
|
+static bool encoder_available(const char *type)
|
|
|
+{
|
|
|
+ auto &encoders = get_available_encoders();
|
|
|
+ return std::find_if(std::begin(encoders), std::end(encoders),
|
|
|
+ [=](const char *encoder) {
|
|
|
+ return strcmp(type, encoder) == 0;
|
|
|
+ }) != std::end(encoders);
|
|
|
+}
|
|
|
+
|
|
|
+static OBSEncoderAutoRelease create_video_encoder(
|
|
|
+ DStr &name_buffer, size_t encoder_index,
|
|
|
+ const GoLiveApi::EncoderConfiguration<
|
|
|
+ GoLiveApi::VideoEncoderConfiguration> &encoder_config)
|
|
|
+{
|
|
|
+ auto encoder_type = encoder_config.config.type.c_str();
|
|
|
+ if (!encoder_available(encoder_type)) {
|
|
|
+ blog(LOG_ERROR, "Encoder type '%s' not available",
|
|
|
+ encoder_type);
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.EncoderNotAvailable")
|
|
|
+ .arg(encoder_type));
|
|
|
+ }
|
|
|
+
|
|
|
+ dstr_printf(name_buffer, "multitrack video video encoder %zu",
|
|
|
+ encoder_index);
|
|
|
+
|
|
|
+ OBSDataAutoRelease encoder_settings =
|
|
|
+ obs_data_create_from_json(encoder_config.data.dump().c_str());
|
|
|
+ obs_data_set_bool(encoder_settings, "disable_scenecut", true);
|
|
|
+
|
|
|
+ OBSEncoderAutoRelease video_encoder = obs_video_encoder_create(
|
|
|
+ encoder_type, name_buffer, encoder_settings, nullptr);
|
|
|
+ if (!video_encoder) {
|
|
|
+ blog(LOG_ERROR, "Failed to create video encoder '%s'",
|
|
|
+ name_buffer->array);
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.FailedToCreateVideoEncoder")
|
|
|
+ .arg(name_buffer->array, encoder_type));
|
|
|
+ }
|
|
|
+ obs_encoder_set_video(video_encoder, obs_get_video());
|
|
|
+
|
|
|
+ obs_video_info ovi;
|
|
|
+ if (!obs_get_video_info(&ovi)) {
|
|
|
+ blog(LOG_WARNING,
|
|
|
+ "Failed to get obs_video_info while creating encoder %zu",
|
|
|
+ encoder_index);
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.FailedToGetOBSVideoInfo")
|
|
|
+ .arg(name_buffer->array, encoder_type));
|
|
|
+ }
|
|
|
+
|
|
|
+ adjust_video_encoder_scaling(ovi, video_encoder, encoder_config.config,
|
|
|
+ encoder_index);
|
|
|
+ adjust_encoder_frame_rate_divisor(ovi, video_encoder,
|
|
|
+ encoder_config.config, encoder_index);
|
|
|
+
|
|
|
+ return video_encoder;
|
|
|
+}
|
|
|
+
|
|
|
+static OBSEncoderAutoRelease create_audio_encoder(const char *name,
|
|
|
+ const char *audio_encoder_id,
|
|
|
+ uint32_t audio_bitrate,
|
|
|
+ size_t mixer_idx)
|
|
|
+{
|
|
|
+ OBSDataAutoRelease settings = obs_data_create();
|
|
|
+ obs_data_set_int(settings, "bitrate", audio_bitrate);
|
|
|
+
|
|
|
+ OBSEncoderAutoRelease audio_encoder = obs_audio_encoder_create(
|
|
|
+ audio_encoder_id, name, settings, mixer_idx, nullptr);
|
|
|
+ if (!audio_encoder) {
|
|
|
+ blog(LOG_ERROR, "Failed to create audio encoder");
|
|
|
+ throw MultitrackVideoError::warning(QTStr(
|
|
|
+ "FailedToStartStream.FailedToCreateAudioEncoder"));
|
|
|
+ }
|
|
|
+ obs_encoder_set_audio(audio_encoder, obs_get_audio());
|
|
|
+ return audio_encoder;
|
|
|
+}
|
|
|
+
|
|
|
+struct OBSOutputs {
|
|
|
+ OBSOutputAutoRelease output, recording_output;
|
|
|
+};
|
|
|
+
|
|
|
+static OBSOutputs
|
|
|
+SetupOBSOutput(obs_data_t *dump_stream_to_file_config,
|
|
|
+ const GoLiveApi::Config &go_live_config,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &audio_encoders,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &video_encoders,
|
|
|
+ const char *audio_encoder_id,
|
|
|
+ std::optional<size_t> vod_track_mixer);
|
|
|
+static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
|
|
|
+ obs_output_t *output, OBSSignal &start,
|
|
|
+ OBSSignal &stop, OBSSignal &deactivate);
|
|
|
+
|
|
|
+void MultitrackVideoOutput::PrepareStreaming(
|
|
|
+ QWidget *parent, const char *service_name, obs_service_t *service,
|
|
|
+ const std::optional<std::string> &rtmp_url, const QString &stream_key,
|
|
|
+ const char *audio_encoder_id,
|
|
|
+ std::optional<uint32_t> maximum_aggregate_bitrate,
|
|
|
+ std::optional<uint32_t> maximum_video_tracks,
|
|
|
+ std::optional<std::string> custom_config,
|
|
|
+ obs_data_t *dump_stream_to_file_config,
|
|
|
+ std::optional<size_t> vod_track_mixer)
|
|
|
+{
|
|
|
+ {
|
|
|
+ const std::lock_guard<std::mutex> current_lock{current_mutex};
|
|
|
+ const std::lock_guard<std::mutex> current_stream_dump_lock{
|
|
|
+ current_stream_dump_mutex};
|
|
|
+ if (current || current_stream_dump) {
|
|
|
+ blog(LOG_WARNING,
|
|
|
+ "Tried to prepare multitrack video output while it's already active");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::optional<GoLiveApi::Config> go_live_config;
|
|
|
+ std::optional<GoLiveApi::Config> custom;
|
|
|
+ bool is_custom_config = custom_config.has_value();
|
|
|
+ auto auto_config_url = MultitrackVideoAutoConfigURL(service);
|
|
|
+
|
|
|
+ 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,
|
|
|
+ "ertmp_multitrack_video_name")) {
|
|
|
+ multitrack_video_name = obs_data_get_string(
|
|
|
+ service_settings, "ertmp_multitrack_video_name");
|
|
|
+ }
|
|
|
+
|
|
|
+ auto auto_config_url_data = auto_config_url.toUtf8();
|
|
|
+
|
|
|
+ DStr vod_track_info_storage;
|
|
|
+ if (vod_track_mixer.has_value())
|
|
|
+ dstr_printf(vod_track_info_storage, "Yes (mixer: %zu)",
|
|
|
+ vod_track_mixer.value());
|
|
|
+
|
|
|
+ blog(LOG_INFO,
|
|
|
+ "Preparing enhanced broadcasting stream for:\n"
|
|
|
+ " custom config: %s\n"
|
|
|
+ " config url: %s\n"
|
|
|
+ " settings:\n"
|
|
|
+ " service: %s\n"
|
|
|
+ " max aggregate bitrate: %s (%" PRIu32 ")\n"
|
|
|
+ " max video tracks: %s (%" PRIu32 ")\n"
|
|
|
+ " custom rtmp url: %s ('%s')\n"
|
|
|
+ " vod track: %s",
|
|
|
+ is_custom_config ? "Yes" : "No",
|
|
|
+ !auto_config_url.isEmpty() ? auto_config_url_data.constData()
|
|
|
+ : "(null)",
|
|
|
+ service_name,
|
|
|
+ maximum_aggregate_bitrate.has_value() ? "Set" : "Auto",
|
|
|
+ maximum_aggregate_bitrate.value_or(0),
|
|
|
+ maximum_video_tracks.has_value() ? "Set" : "Auto",
|
|
|
+ maximum_video_tracks.value_or(0),
|
|
|
+ rtmp_url.has_value() ? "Yes" : "No",
|
|
|
+ rtmp_url.has_value() ? rtmp_url->c_str() : "",
|
|
|
+ vod_track_info_storage->array ? vod_track_info_storage->array
|
|
|
+ : "No");
|
|
|
+
|
|
|
+ const bool custom_config_only =
|
|
|
+ auto_config_url.isEmpty() &&
|
|
|
+ MultitrackVideoDeveloperModeEnabled() &&
|
|
|
+ custom_config.has_value() &&
|
|
|
+ strcmp(obs_service_get_id(service), "rtmp_custom") == 0;
|
|
|
+
|
|
|
+ if (!custom_config_only) {
|
|
|
+ auto go_live_post = constructGoLivePost(
|
|
|
+ stream_key, maximum_aggregate_bitrate,
|
|
|
+ maximum_video_tracks, vod_track_mixer.has_value());
|
|
|
+
|
|
|
+ go_live_config = DownloadGoLiveConfig(parent, auto_config_url,
|
|
|
+ go_live_post,
|
|
|
+ multitrack_video_name);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (custom_config.has_value()) {
|
|
|
+ GoLiveApi::Config parsed_custom;
|
|
|
+ try {
|
|
|
+ parsed_custom = nlohmann::json::parse(*custom_config);
|
|
|
+ } catch (const nlohmann::json::exception &exception) {
|
|
|
+ blog(LOG_WARNING, "Failed to parse custom config: %s",
|
|
|
+ exception.what());
|
|
|
+ throw MultitrackVideoError::critical(QTStr(
|
|
|
+ "FailedToStartStream.InvalidCustomConfig"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // copy unique ID from go live request
|
|
|
+ if (go_live_config.has_value()) {
|
|
|
+ parsed_custom.meta.config_id =
|
|
|
+ go_live_config->meta.config_id;
|
|
|
+ blog(LOG_INFO,
|
|
|
+ "Using config_id from go live config with custom config: %s",
|
|
|
+ parsed_custom.meta.config_id.c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ nlohmann::json custom_data = parsed_custom;
|
|
|
+ blog(LOG_INFO, "Using custom go live config: %s",
|
|
|
+ custom_data.dump(4).c_str());
|
|
|
+
|
|
|
+ custom.emplace(std::move(parsed_custom));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (go_live_config.has_value()) {
|
|
|
+ blog(LOG_INFO, "Enhanced broadcasting config_id: '%s'",
|
|
|
+ go_live_config->meta.config_id.c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!go_live_config && !custom) {
|
|
|
+ blog(LOG_ERROR,
|
|
|
+ "MultitrackVideoOutput: no config set, this should never happen");
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.NoConfig"));
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto &output_config = custom ? *custom : *go_live_config;
|
|
|
+ const auto &service_config = go_live_config ? *go_live_config : *custom;
|
|
|
+
|
|
|
+ auto audio_encoders = std::vector<OBSEncoderAutoRelease>();
|
|
|
+ auto video_encoders = std::vector<OBSEncoderAutoRelease>();
|
|
|
+ auto outputs = SetupOBSOutput(dump_stream_to_file_config, output_config,
|
|
|
+ audio_encoders, video_encoders,
|
|
|
+ audio_encoder_id, vod_track_mixer);
|
|
|
+ auto output = std::move(outputs.output);
|
|
|
+ auto recording_output = std::move(outputs.recording_output);
|
|
|
+ if (!output)
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.FallbackToDefault")
|
|
|
+ .arg(multitrack_video_name));
|
|
|
+
|
|
|
+ auto multitrack_video_service =
|
|
|
+ create_service(service_config, rtmp_url, stream_key);
|
|
|
+ if (!multitrack_video_service)
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.FallbackToDefault")
|
|
|
+ .arg(multitrack_video_name));
|
|
|
+
|
|
|
+ obs_output_set_service(output, multitrack_video_service);
|
|
|
+
|
|
|
+ OBSSignal start_streaming;
|
|
|
+ OBSSignal stop_streaming;
|
|
|
+ OBSSignal deactivate_stream;
|
|
|
+ SetupSignalHandlers(false, this, output, start_streaming,
|
|
|
+ stop_streaming, deactivate_stream);
|
|
|
+
|
|
|
+ if (dump_stream_to_file_config && recording_output) {
|
|
|
+ OBSSignal start_recording;
|
|
|
+ OBSSignal stop_recording;
|
|
|
+ OBSSignal deactivate_recording;
|
|
|
+ SetupSignalHandlers(true, this, recording_output,
|
|
|
+ start_recording, stop_recording,
|
|
|
+ deactivate_recording);
|
|
|
+
|
|
|
+ decltype(video_encoders) recording_video_encoders;
|
|
|
+ recording_video_encoders.reserve(video_encoders.size());
|
|
|
+ for (auto &encoder : video_encoders) {
|
|
|
+ recording_video_encoders.emplace_back(
|
|
|
+ obs_encoder_get_ref(encoder));
|
|
|
+ }
|
|
|
+
|
|
|
+ decltype(audio_encoders) recording_audio_encoders;
|
|
|
+ recording_audio_encoders.reserve(audio_encoders.size());
|
|
|
+ for (auto &encoder : audio_encoders) {
|
|
|
+ recording_audio_encoders.emplace_back(
|
|
|
+ obs_encoder_get_ref(encoder));
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ const std::lock_guard current_stream_dump_lock{
|
|
|
+ current_stream_dump_mutex};
|
|
|
+ current_stream_dump.emplace(OBSOutputObjects{
|
|
|
+ std::move(recording_output),
|
|
|
+ std::move(recording_video_encoders),
|
|
|
+ std::move(recording_audio_encoders),
|
|
|
+ nullptr,
|
|
|
+ std::move(start_recording),
|
|
|
+ std::move(stop_recording),
|
|
|
+ std::move(deactivate_recording),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const std::lock_guard current_lock{current_mutex};
|
|
|
+ current.emplace(OBSOutputObjects{
|
|
|
+ std::move(output),
|
|
|
+ std::move(video_encoders),
|
|
|
+ std::move(audio_encoders),
|
|
|
+ std::move(multitrack_video_service),
|
|
|
+ std::move(start_streaming),
|
|
|
+ std::move(stop_streaming),
|
|
|
+ std::move(deactivate_stream),
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+signal_handler_t *MultitrackVideoOutput::StreamingSignalHandler()
|
|
|
+{
|
|
|
+ const std::lock_guard current_lock{current_mutex};
|
|
|
+ return current.has_value()
|
|
|
+ ? obs_output_get_signal_handler(current->output_)
|
|
|
+ : nullptr;
|
|
|
+}
|
|
|
+
|
|
|
+void MultitrackVideoOutput::StartedStreaming()
|
|
|
+{
|
|
|
+ OBSOutputAutoRelease dump_output;
|
|
|
+ {
|
|
|
+ const std::lock_guard current_stream_dump_lock{
|
|
|
+ current_stream_dump_mutex};
|
|
|
+ if (current_stream_dump && current_stream_dump->output_) {
|
|
|
+ dump_output = obs_output_get_ref(
|
|
|
+ current_stream_dump->output_);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dump_output)
|
|
|
+ return;
|
|
|
+
|
|
|
+ auto result = obs_output_start(dump_output);
|
|
|
+ blog(LOG_INFO, "MultitrackVideoOutput: starting recording%s",
|
|
|
+ result ? "" : " failed");
|
|
|
+}
|
|
|
+
|
|
|
+void MultitrackVideoOutput::StopStreaming()
|
|
|
+{
|
|
|
+ OBSOutputAutoRelease current_output;
|
|
|
+ {
|
|
|
+ const std::lock_guard current_lock{current_mutex};
|
|
|
+ if (current && current->output_)
|
|
|
+ current_output = obs_output_get_ref(current->output_);
|
|
|
+ }
|
|
|
+ if (current_output)
|
|
|
+ obs_output_stop(current_output);
|
|
|
+
|
|
|
+ OBSOutputAutoRelease dump_output;
|
|
|
+ {
|
|
|
+ const std::lock_guard current_stream_dump_lock{
|
|
|
+ current_stream_dump_mutex};
|
|
|
+ if (current_stream_dump && current_stream_dump->output_)
|
|
|
+ dump_output = obs_output_get_ref(
|
|
|
+ current_stream_dump->output_);
|
|
|
+ }
|
|
|
+ if (dump_output)
|
|
|
+ obs_output_stop(dump_output);
|
|
|
+}
|
|
|
+
|
|
|
+bool MultitrackVideoOutput::HandleIncompatibleSettings(
|
|
|
+ QWidget *parent, config_t *config, obs_service_t *service,
|
|
|
+ bool &useDelay, bool &enableNewSocketLoop, bool &enableDynBitrate)
|
|
|
+{
|
|
|
+ QString incompatible_settings;
|
|
|
+ QString where_to_disable;
|
|
|
+ QString incompatible_settings_list;
|
|
|
+
|
|
|
+ size_t num = 1;
|
|
|
+
|
|
|
+ auto check_setting = [&](bool setting, const char *name,
|
|
|
+ const char *section) {
|
|
|
+ if (!setting)
|
|
|
+ return;
|
|
|
+
|
|
|
+ incompatible_settings +=
|
|
|
+ QString(" %1. %2\n").arg(num).arg(QTStr(name));
|
|
|
+
|
|
|
+ where_to_disable +=
|
|
|
+ QString(" %1. [%2 > %3 > %4]\n")
|
|
|
+ .arg(num)
|
|
|
+ .arg(QTStr("Settings"))
|
|
|
+ .arg(QTStr("Basic.Settings.Advanced"))
|
|
|
+ .arg(QTStr(section));
|
|
|
+
|
|
|
+ incompatible_settings_list += QString("%1, ").arg(name);
|
|
|
+
|
|
|
+ num += 1;
|
|
|
+ };
|
|
|
+
|
|
|
+ check_setting(useDelay, "Basic.Settings.Advanced.StreamDelay",
|
|
|
+ "Basic.Settings.Advanced.StreamDelay");
|
|
|
+#ifdef _WIN32
|
|
|
+ check_setting(enableNewSocketLoop,
|
|
|
+ "Basic.Settings.Advanced.Network.EnableNewSocketLoop",
|
|
|
+ "Basic.Settings.Advanced.Network");
|
|
|
+#endif
|
|
|
+ check_setting(enableDynBitrate,
|
|
|
+ "Basic.Settings.Output.DynamicBitrate.Beta",
|
|
|
+ "Basic.Settings.Advanced.Network");
|
|
|
+
|
|
|
+ if (incompatible_settings.isEmpty())
|
|
|
+ return true;
|
|
|
+
|
|
|
+ OBSDataAutoRelease service_settings = obs_service_get_settings(service);
|
|
|
+
|
|
|
+ QMessageBox mb(parent);
|
|
|
+ mb.setIcon(QMessageBox::Critical);
|
|
|
+ mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
|
|
|
+ mb.setText(
|
|
|
+ QString(QTStr("MultitrackVideo.IncompatibleSettings.Text"))
|
|
|
+ .arg(obs_data_get_string(service_settings,
|
|
|
+ "ertmp_multitrack_video_name"))
|
|
|
+ .arg(incompatible_settings)
|
|
|
+ .arg(where_to_disable));
|
|
|
+ auto this_stream = mb.addButton(
|
|
|
+ QTStr("MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming"),
|
|
|
+ QMessageBox::AcceptRole);
|
|
|
+ auto all_streams = mb.addButton(
|
|
|
+ QString(QTStr(
|
|
|
+ "MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming")),
|
|
|
+ QMessageBox::AcceptRole);
|
|
|
+ mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
|
|
|
+
|
|
|
+ mb.exec();
|
|
|
+
|
|
|
+ const char *action = "cancel";
|
|
|
+ if (mb.clickedButton() == this_stream) {
|
|
|
+ action = "DisableAndStartStreaming";
|
|
|
+ } else if (mb.clickedButton() == all_streams) {
|
|
|
+ action = "UpdateAndStartStreaming";
|
|
|
+ }
|
|
|
+
|
|
|
+ blog(LOG_INFO,
|
|
|
+ "MultitrackVideoOutput: attempted to start stream with incompatible"
|
|
|
+ "settings (%s); action taken: %s",
|
|
|
+ incompatible_settings_list.toUtf8().constData(), action);
|
|
|
+
|
|
|
+ if (mb.clickedButton() == this_stream ||
|
|
|
+ mb.clickedButton() == all_streams) {
|
|
|
+ useDelay = false;
|
|
|
+ enableNewSocketLoop = false;
|
|
|
+ enableDynBitrate = false;
|
|
|
+ useDelay = false;
|
|
|
+ enableNewSocketLoop = false;
|
|
|
+ enableDynBitrate = false;
|
|
|
+
|
|
|
+ if (mb.clickedButton() == all_streams) {
|
|
|
+ config_set_bool(config, "Output", "DelayEnable", false);
|
|
|
+#ifdef _WIN32
|
|
|
+ config_set_bool(config, "Output", "NewSocketLoopEnable",
|
|
|
+ false);
|
|
|
+#endif
|
|
|
+ config_set_bool(config, "Output", "DynamicBitrate",
|
|
|
+ false);
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+static bool
|
|
|
+create_video_encoders(const GoLiveApi::Config &go_live_config,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &video_encoders,
|
|
|
+ obs_output_t *output, obs_output_t *recording_output)
|
|
|
+{
|
|
|
+ DStr video_encoder_name_buffer;
|
|
|
+ obs_encoder_t *first_encoder = nullptr;
|
|
|
+ if (go_live_config.encoder_configurations.empty()) {
|
|
|
+ blog(LOG_WARNING,
|
|
|
+ "MultitrackVideoOutput: Missing video encoder configurations");
|
|
|
+ throw MultitrackVideoError::warning(
|
|
|
+ QTStr("FailedToStartStream.MissingEncoderConfigs"));
|
|
|
+ }
|
|
|
+
|
|
|
+ for (size_t i = 0; i < go_live_config.encoder_configurations.size();
|
|
|
+ i++) {
|
|
|
+ auto encoder = create_video_encoder(
|
|
|
+ video_encoder_name_buffer, i,
|
|
|
+ go_live_config.encoder_configurations[i]);
|
|
|
+ if (!encoder)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (!first_encoder)
|
|
|
+ first_encoder = encoder;
|
|
|
+ else
|
|
|
+ obs_encoder_group_keyframe_aligned_encoders(
|
|
|
+ first_encoder, encoder);
|
|
|
+
|
|
|
+ obs_output_set_video_encoder2(output, encoder, i);
|
|
|
+ if (recording_output)
|
|
|
+ obs_output_set_video_encoder2(recording_output, encoder,
|
|
|
+ i);
|
|
|
+ video_encoders.emplace_back(std::move(encoder));
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static void
|
|
|
+create_audio_encoders(const GoLiveApi::Config &go_live_config,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &audio_encoders,
|
|
|
+ obs_output_t *output, obs_output_t *recording_output,
|
|
|
+ const char *audio_encoder_id,
|
|
|
+ std::optional<size_t> vod_track_mixer)
|
|
|
+{
|
|
|
+ using encoder_configs_type =
|
|
|
+ decltype(go_live_config.audio_configurations.live);
|
|
|
+ DStr encoder_name_buffer;
|
|
|
+ size_t output_encoder_index = 0;
|
|
|
+
|
|
|
+ auto create_encoders = [&](const char *name_prefix,
|
|
|
+ const encoder_configs_type &configs,
|
|
|
+ size_t mixer_idx) {
|
|
|
+ if (configs.empty()) {
|
|
|
+ blog(LOG_WARNING,
|
|
|
+ "MultitrackVideoOutput: Missing audio encoder configurations (for '%s')",
|
|
|
+ name_prefix);
|
|
|
+ throw MultitrackVideoError::warning(QTStr(
|
|
|
+ "FailedToStartStream.MissingEncoderConfigs"));
|
|
|
+ }
|
|
|
+
|
|
|
+ for (size_t i = 0; i < configs.size(); i++) {
|
|
|
+ dstr_printf(encoder_name_buffer, "%s %zu", name_prefix,
|
|
|
+ i);
|
|
|
+ OBSEncoderAutoRelease audio_encoder =
|
|
|
+ create_audio_encoder(encoder_name_buffer->array,
|
|
|
+ audio_encoder_id,
|
|
|
+ configs[i].config.bitrate,
|
|
|
+ mixer_idx);
|
|
|
+ obs_output_set_audio_encoder(output, audio_encoder,
|
|
|
+ output_encoder_index);
|
|
|
+ if (recording_output)
|
|
|
+ obs_output_set_audio_encoder(
|
|
|
+ recording_output, audio_encoder,
|
|
|
+ output_encoder_index);
|
|
|
+ output_encoder_index += 1;
|
|
|
+ audio_encoders.emplace_back(std::move(audio_encoder));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ create_encoders("multitrack video live audio",
|
|
|
+ go_live_config.audio_configurations.live, 0);
|
|
|
+
|
|
|
+ if (!vod_track_mixer.has_value())
|
|
|
+ return;
|
|
|
+
|
|
|
+ create_encoders("multitrack video vod audio",
|
|
|
+ go_live_config.audio_configurations.vod,
|
|
|
+ *vod_track_mixer);
|
|
|
+
|
|
|
+ return;
|
|
|
+}
|
|
|
+
|
|
|
+static OBSOutputs
|
|
|
+SetupOBSOutput(obs_data_t *dump_stream_to_file_config,
|
|
|
+ const GoLiveApi::Config &go_live_config,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &audio_encoders,
|
|
|
+ std::vector<OBSEncoderAutoRelease> &video_encoders,
|
|
|
+ const char *audio_encoder_id,
|
|
|
+ std::optional<size_t> vod_track_mixer)
|
|
|
+{
|
|
|
+
|
|
|
+ auto output = create_output();
|
|
|
+ OBSOutputAutoRelease recording_output;
|
|
|
+ if (dump_stream_to_file_config)
|
|
|
+ recording_output =
|
|
|
+ create_recording_output(dump_stream_to_file_config);
|
|
|
+
|
|
|
+ if (!create_video_encoders(go_live_config, video_encoders, output,
|
|
|
+ recording_output))
|
|
|
+ return {nullptr, nullptr};
|
|
|
+
|
|
|
+ create_audio_encoders(go_live_config, audio_encoders, output,
|
|
|
+ recording_output, audio_encoder_id,
|
|
|
+ vod_track_mixer);
|
|
|
+
|
|
|
+ return {std::move(output), std::move(recording_output)};
|
|
|
+}
|
|
|
+
|
|
|
+void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self,
|
|
|
+ obs_output_t *output, OBSSignal &start,
|
|
|
+ OBSSignal &stop, OBSSignal &deactivate)
|
|
|
+{
|
|
|
+ auto handler = obs_output_get_signal_handler(output);
|
|
|
+
|
|
|
+ if (recording)
|
|
|
+ start.Connect(handler, "start", RecordingStartHandler, self);
|
|
|
+
|
|
|
+ stop.Connect(handler, "stop",
|
|
|
+ !recording ? StreamStopHandler : RecordingStopHandler,
|
|
|
+ self);
|
|
|
+
|
|
|
+ deactivate.Connect(handler, "deactivate",
|
|
|
+ !recording ? StreamDeactivateHandler
|
|
|
+ : RecordingDeactivateHandler,
|
|
|
+ self);
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<MultitrackVideoOutput::OBSOutputObjects>
|
|
|
+MultitrackVideoOutput::take_current()
|
|
|
+{
|
|
|
+ const std::lock_guard<std::mutex> current_lock{current_mutex};
|
|
|
+ auto val = std::move(current);
|
|
|
+ current.reset();
|
|
|
+ return val;
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<MultitrackVideoOutput::OBSOutputObjects>
|
|
|
+MultitrackVideoOutput::take_current_stream_dump()
|
|
|
+{
|
|
|
+ const std::lock_guard<std::mutex> current_stream_dump_lock{
|
|
|
+ current_stream_dump_mutex};
|
|
|
+ auto val = std::move(current_stream_dump);
|
|
|
+ current_stream_dump.reset();
|
|
|
+ return val;
|
|
|
+}
|
|
|
+
|
|
|
+void MultitrackVideoOutput::ReleaseOnMainThread(
|
|
|
+ std::optional<OBSOutputObjects> objects)
|
|
|
+{
|
|
|
+
|
|
|
+ if (!objects.has_value())
|
|
|
+ return;
|
|
|
+
|
|
|
+ QMetaObject::invokeMethod(
|
|
|
+ QApplication::instance()->thread(),
|
|
|
+ [objects = std::move(objects)] {}, Qt::QueuedConnection);
|
|
|
+}
|
|
|
+
|
|
|
+void StreamStopHandler(void *arg, calldata_t *params)
|
|
|
+{
|
|
|
+ auto self = static_cast<MultitrackVideoOutput *>(arg);
|
|
|
+
|
|
|
+ OBSOutputAutoRelease stream_dump_output;
|
|
|
+ {
|
|
|
+ const std::lock_guard<std::mutex> current_stream_dump_lock{
|
|
|
+ self->current_stream_dump_mutex};
|
|
|
+ if (self->current_stream_dump &&
|
|
|
+ self->current_stream_dump->output_)
|
|
|
+ stream_dump_output = obs_output_get_ref(
|
|
|
+ self->current_stream_dump->output_);
|
|
|
+ }
|
|
|
+ if (stream_dump_output)
|
|
|
+ obs_output_stop(stream_dump_output);
|
|
|
+
|
|
|
+ if (obs_output_active(static_cast<obs_output_t *>(
|
|
|
+ calldata_ptr(params, "output"))))
|
|
|
+ return;
|
|
|
+
|
|
|
+ MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
|
|
|
+}
|
|
|
+
|
|
|
+void StreamDeactivateHandler(void *arg, calldata_t *params)
|
|
|
+{
|
|
|
+ auto self = static_cast<MultitrackVideoOutput *>(arg);
|
|
|
+
|
|
|
+ if (obs_output_reconnecting(static_cast<obs_output_t *>(
|
|
|
+ calldata_ptr(params, "output"))))
|
|
|
+ return;
|
|
|
+
|
|
|
+ MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
|
|
|
+}
|
|
|
+
|
|
|
+void RecordingStartHandler(void * /* arg */, calldata_t * /* data */)
|
|
|
+{
|
|
|
+ blog(LOG_INFO, "MultitrackVideoOutput: recording started");
|
|
|
+}
|
|
|
+
|
|
|
+void RecordingStopHandler(void *arg, calldata_t *params)
|
|
|
+{
|
|
|
+ auto self = static_cast<MultitrackVideoOutput *>(arg);
|
|
|
+ blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
|
|
|
+
|
|
|
+ if (obs_output_active(static_cast<obs_output_t *>(
|
|
|
+ calldata_ptr(params, "output"))))
|
|
|
+ return;
|
|
|
+
|
|
|
+ MultitrackVideoOutput::ReleaseOnMainThread(
|
|
|
+ self->take_current_stream_dump());
|
|
|
+}
|
|
|
+
|
|
|
+void RecordingDeactivateHandler(void *arg, calldata_t * /*data*/)
|
|
|
+{
|
|
|
+ auto self = static_cast<MultitrackVideoOutput *>(arg);
|
|
|
+ MultitrackVideoOutput::ReleaseOnMainThread(
|
|
|
+ self->take_current_stream_dump());
|
|
|
+}
|