1
0

MultitrackVideoOutput.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. #include "MultitrackVideoError.hpp"
  2. #include "MultitrackVideoOutput.hpp"
  3. #include "models/multitrack-video.hpp"
  4. #include "GoLiveAPI_Network.hpp"
  5. #include "GoLiveAPI_PostData.hpp"
  6. #include <OBSApp.hpp>
  7. #include <bpm.h>
  8. #include <util/dstr.hpp>
  9. #include <libavformat/avformat.h>
  10. #include <QPushButton>
  11. #include <QMessageBox>
  12. #include <QThread>
  13. #include <QUrl>
  14. #include <QUrlQuery>
  15. #include <cinttypes>
  16. // Codec profile strings
  17. static const char *h264_main = "Main";
  18. static const char *h264_high = "High";
  19. static const char *h264_cb = "Constrained Baseline";
  20. static const char *hevc_main = "Main";
  21. static const char *hevc_main10 = "Main 10";
  22. static const char *av1_main = "Main";
  23. // Maximum reconnect attempts with an invalid key error before giving up (roughly 30 seconds with default start value)
  24. static constexpr uint8_t MAX_RECONNECT_ATTEMPTS = 5;
  25. Qt::ConnectionType BlockingConnectionTypeFor(QObject *object)
  26. {
  27. return object->thread() == QThread::currentThread() ? Qt::DirectConnection : Qt::BlockingQueuedConnection;
  28. }
  29. bool MultitrackVideoDeveloperModeEnabled()
  30. {
  31. static bool developer_mode = [] {
  32. auto args = qApp->arguments();
  33. for (const auto &arg : args) {
  34. if (arg == "--enable-multitrack-video-dev") {
  35. return true;
  36. }
  37. }
  38. return false;
  39. }();
  40. return developer_mode;
  41. }
  42. static OBSServiceAutoRelease create_service(const GoLiveApi::Config &go_live_config,
  43. const std::optional<std::string> &rtmp_url, const QString &in_stream_key,
  44. std::optional<bool> use_rtmps)
  45. {
  46. const char *url = nullptr;
  47. QString stream_key = in_stream_key;
  48. const auto &ingest_endpoints = go_live_config.ingest_endpoints;
  49. for (auto &endpoint : ingest_endpoints) {
  50. if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4))
  51. continue;
  52. if (use_rtmps.has_value() && *use_rtmps != (qstricmp("RTMPS", endpoint.protocol.c_str()) == 0))
  53. continue;
  54. url = endpoint.url_template.c_str();
  55. if (endpoint.authentication && !endpoint.authentication->empty()) {
  56. blog(LOG_INFO, "Using stream key supplied by autoconfig");
  57. stream_key = QString::fromStdString(*endpoint.authentication);
  58. }
  59. break;
  60. }
  61. if (rtmp_url.has_value()) {
  62. // Despite being set by user, it was set to a ""
  63. if (rtmp_url->empty()) {
  64. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.NoCustomRTMPURLInSettings"));
  65. }
  66. url = rtmp_url->c_str();
  67. blog(LOG_INFO, "Using custom RTMP URL: '%s'", url);
  68. } else {
  69. if (!url) {
  70. blog(LOG_ERROR, "No RTMP URL in go live config");
  71. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.NoRTMPURLInConfig"));
  72. }
  73. blog(LOG_INFO, "Using URL template: '%s'", url);
  74. }
  75. DStr str;
  76. dstr_cat(str, url);
  77. // dstr_find does not protect against null, and dstr_cat will
  78. // not initialize str if cat'ing with a null url
  79. if (!dstr_is_empty(str)) {
  80. auto found = dstr_find(str, "/{stream_key}");
  81. if (found)
  82. dstr_remove(str, found - str->array, str->len - (found - str->array));
  83. }
  84. /* The stream key itself may contain query parameters, such as
  85. * "bandwidthtest" that need to be carried over. */
  86. QUrl parsed_user_key{in_stream_key};
  87. QUrlQuery user_key_query{parsed_user_key};
  88. QUrl parsed_key{stream_key};
  89. QUrl parsed_url{url};
  90. QUrlQuery parsed_query{parsed_url};
  91. for (const auto &[key, value] : user_key_query.queryItems())
  92. parsed_query.addQueryItem(key, value);
  93. if (!go_live_config.meta.config_id.empty()) {
  94. parsed_query.addQueryItem("clientConfigId", QString::fromStdString(go_live_config.meta.config_id));
  95. }
  96. parsed_key.setQuery(parsed_query);
  97. OBSDataAutoRelease settings = obs_data_create();
  98. obs_data_set_string(settings, "server", str->array);
  99. obs_data_set_string(settings, "key", parsed_key.toString().toUtf8().constData());
  100. auto service = obs_service_create("rtmp_custom", "multitrack video service", settings, nullptr);
  101. if (!service) {
  102. blog(LOG_WARNING, "Failed to create multitrack video service");
  103. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.FailedToCreateMultitrackVideoService"));
  104. }
  105. return service;
  106. }
  107. static OBSOutputAutoRelease create_output()
  108. {
  109. OBSOutputAutoRelease output = obs_output_create("rtmp_output", "rtmp multitrack video", nullptr, nullptr);
  110. if (!output) {
  111. blog(LOG_ERROR, "Failed to create multitrack video rtmp output");
  112. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.FailedToCreateMultitrackVideoOutput"));
  113. }
  114. return output;
  115. }
  116. static OBSOutputAutoRelease create_recording_output(obs_data_t *settings)
  117. {
  118. OBSOutputAutoRelease output;
  119. bool useMP4 = obs_data_get_bool(settings, "use_mp4");
  120. if (useMP4) {
  121. output = obs_output_create("mp4_output", "mp4 multitrack video", settings, nullptr);
  122. } else {
  123. output = obs_output_create("flv_output", "flv multitrack video", settings, nullptr);
  124. }
  125. if (!output) {
  126. blog(LOG_ERROR, "Failed to create multitrack video %s output", useMP4 ? "mp4" : "flv");
  127. }
  128. return output;
  129. }
  130. static void adjust_video_encoder_scaling(const obs_video_info &ovi, obs_encoder_t *video_encoder,
  131. const GoLiveApi::VideoEncoderConfiguration &encoder_config,
  132. size_t encoder_index)
  133. {
  134. auto requested_width = encoder_config.width;
  135. auto requested_height = encoder_config.height;
  136. if (ovi.base_width < requested_width || ovi.base_height < requested_height) {
  137. blog(LOG_WARNING,
  138. "Requested resolution exceeds canvas/available resolution for encoder %zu: %" PRIu32 "x%" PRIu32
  139. " > %" PRIu32 "x%" PRIu32,
  140. encoder_index, requested_width, requested_height, ovi.base_width, ovi.base_height);
  141. }
  142. obs_encoder_set_scaled_size(video_encoder, requested_width, requested_height);
  143. obs_encoder_set_gpu_scale_type(video_encoder, encoder_config.gpu_scale_type.value_or(OBS_SCALE_BICUBIC));
  144. obs_encoder_set_preferred_video_format(video_encoder, encoder_config.format.value_or(VIDEO_FORMAT_NV12));
  145. obs_encoder_set_preferred_color_space(video_encoder, encoder_config.colorspace.value_or(VIDEO_CS_709));
  146. obs_encoder_set_preferred_range(video_encoder, encoder_config.range.value_or(VIDEO_RANGE_PARTIAL));
  147. }
  148. static uint32_t closest_divisor(const obs_video_info &ovi, const media_frames_per_second &target_fps)
  149. {
  150. auto target = (uint64_t)target_fps.numerator * ovi.fps_den;
  151. auto source = (uint64_t)ovi.fps_num * target_fps.denominator;
  152. return std::max(1u, static_cast<uint32_t>(source / target));
  153. }
  154. static void adjust_encoder_frame_rate_divisor(const obs_video_info &ovi, obs_encoder_t *video_encoder,
  155. const GoLiveApi::VideoEncoderConfiguration &encoder_config,
  156. const size_t encoder_index)
  157. {
  158. if (!encoder_config.framerate) {
  159. blog(LOG_WARNING, "`framerate` not specified for encoder %zu", encoder_index);
  160. return;
  161. }
  162. media_frames_per_second requested_fps = *encoder_config.framerate;
  163. if (ovi.fps_num == requested_fps.numerator && ovi.fps_den == requested_fps.denominator)
  164. return;
  165. auto divisor = closest_divisor(ovi, requested_fps);
  166. if (divisor <= 1)
  167. return;
  168. blog(LOG_INFO, "Setting frame rate divisor to %u for encoder %zu", divisor, encoder_index);
  169. obs_encoder_set_frame_rate_divisor(video_encoder, divisor);
  170. }
  171. static bool encoder_available(const char *type)
  172. {
  173. const char *id = nullptr;
  174. for (size_t idx = 0; obs_enum_encoder_types(idx, &id); idx++) {
  175. if (strcmp(id, type) == 0)
  176. return true;
  177. }
  178. return false;
  179. }
  180. static OBSEncoderAutoRelease create_video_encoder(DStr &name_buffer, size_t encoder_index,
  181. const GoLiveApi::VideoEncoderConfiguration &encoder_config,
  182. const OBSCanvasAutoRelease &canvas)
  183. {
  184. auto encoder_type = encoder_config.type.c_str();
  185. if (!encoder_available(encoder_type)) {
  186. blog(LOG_ERROR, "Encoder type '%s' not available", encoder_type);
  187. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.EncoderNotAvailable").arg(encoder_type));
  188. }
  189. dstr_printf(name_buffer, "multitrack video video encoder %zu", encoder_index);
  190. OBSDataAutoRelease encoder_settings = obs_data_create_from_json(encoder_config.settings.dump().c_str());
  191. /* VAAPI-based encoders unfortunately use an integer for "profile". Until a string-based "profile" can be used with
  192. * VAAPI, find the corresponding integer value and update the settings with an integer-based "profile".
  193. */
  194. if (strstr(encoder_type, "vaapi")) {
  195. // Move the "profile" string to "profile_str".
  196. const char *profile_str = obs_data_get_string(encoder_settings, "profile");
  197. obs_data_set_string(encoder_settings, "profile_str", profile_str);
  198. obs_data_item_t *profile_item = obs_data_item_byname(encoder_settings, "profile");
  199. obs_data_item_remove(&profile_item);
  200. obs_data_item_release(&profile_item);
  201. // Find the vaapi_profile integer based on codec type and "profile" string.
  202. int vaapi_profile;
  203. const char *codec = obs_get_encoder_codec(encoder_type);
  204. if (strcmp(codec, "h264") == 0) {
  205. if (astrcmpi(profile_str, h264_main) == 0) {
  206. vaapi_profile = AV_PROFILE_H264_MAIN;
  207. } else if (astrcmpi(profile_str, h264_high) == 0) {
  208. vaapi_profile = AV_PROFILE_H264_HIGH;
  209. } else if (astrcmpi(profile_str, h264_cb) == 0) {
  210. vaapi_profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
  211. } else {
  212. blog(LOG_WARNING, "Unsupported H264 profile '%s', setting to Main profile",
  213. profile_str);
  214. vaapi_profile = AV_PROFILE_H264_MAIN;
  215. }
  216. } else if (strcmp(codec, "hevc") == 0) {
  217. if (astrcmpi(profile_str, hevc_main) == 0) {
  218. vaapi_profile = AV_PROFILE_HEVC_MAIN;
  219. } else if (astrcmpi(profile_str, hevc_main10) == 0) {
  220. vaapi_profile = AV_PROFILE_HEVC_MAIN_10;
  221. } else {
  222. blog(LOG_WARNING, "Unsupported HEVC profile '%s', setting to Main profile",
  223. profile_str);
  224. vaapi_profile = AV_PROFILE_HEVC_MAIN;
  225. }
  226. } else if (strcmp(codec, "av1") == 0) {
  227. if (astrcmpi(profile_str, av1_main) == 0) {
  228. vaapi_profile = AV_PROFILE_AV1_MAIN;
  229. } else {
  230. blog(LOG_WARNING, "Unsupported AV1 profile '%s', setting to Main profile", profile_str);
  231. vaapi_profile = AV_PROFILE_AV1_MAIN;
  232. }
  233. } else {
  234. vaapi_profile = AV_PROFILE_UNKNOWN;
  235. blog(LOG_WARNING, "Unsupported codec '%s', setting profile to unknown", codec);
  236. }
  237. obs_data_set_int(encoder_settings, "profile", vaapi_profile);
  238. }
  239. obs_data_set_bool(encoder_settings, "disable_scenecut", true);
  240. OBSEncoderAutoRelease video_encoder =
  241. obs_video_encoder_create(encoder_type, name_buffer, encoder_settings, nullptr);
  242. if (!video_encoder) {
  243. blog(LOG_ERROR, "Failed to create video encoder '%s'", name_buffer->array);
  244. throw MultitrackVideoError::warning(
  245. QTStr("FailedToStartStream.FailedToCreateVideoEncoder").arg(name_buffer->array, encoder_type));
  246. }
  247. obs_encoder_set_video(video_encoder, obs_canvas_get_video(canvas));
  248. obs_video_info ovi;
  249. if (!obs_canvas_get_video_info(canvas, &ovi)) {
  250. blog(LOG_WARNING, "Failed to get obs_video_info while creating encoder %zu", encoder_index);
  251. throw MultitrackVideoError::warning(
  252. QTStr("FailedToStartStream.FailedToGetOBSVideoInfo").arg(name_buffer->array, encoder_type));
  253. }
  254. adjust_video_encoder_scaling(ovi, video_encoder, encoder_config, encoder_index);
  255. adjust_encoder_frame_rate_divisor(ovi, video_encoder, encoder_config, encoder_index);
  256. return video_encoder;
  257. }
  258. static OBSEncoderAutoRelease create_audio_encoder(const char *name, const char *audio_encoder_id, obs_data_t *settings,
  259. size_t mixer_idx)
  260. {
  261. OBSEncoderAutoRelease audio_encoder =
  262. obs_audio_encoder_create(audio_encoder_id, name, settings, mixer_idx, nullptr);
  263. if (!audio_encoder) {
  264. blog(LOG_ERROR, "Failed to create audio encoder");
  265. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.FailedToCreateAudioEncoder"));
  266. }
  267. obs_encoder_set_audio(audio_encoder, obs_get_audio());
  268. return audio_encoder;
  269. }
  270. struct OBSOutputs {
  271. OBSOutputAutoRelease output, recording_output;
  272. };
  273. static OBSOutputs SetupOBSOutput(QWidget *parent, const QString &multitrack_video_name,
  274. obs_data_t *dump_stream_to_file_config, const GoLiveApi::Config &go_live_config,
  275. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  276. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  277. const char *audio_encoder_id, size_t main_audio_mixer,
  278. std::optional<size_t> vod_track_mixer,
  279. const std::vector<OBSCanvasAutoRelease> &canvases);
  280. static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, obs_output_t *output, OBSSignal &start,
  281. OBSSignal &stop);
  282. void MultitrackVideoOutput::PrepareStreaming(
  283. QWidget *parent, const char *service_name, obs_service_t *service, const std::optional<std::string> &rtmp_url,
  284. const QString &stream_key, const char *audio_encoder_id, std::optional<uint32_t> maximum_aggregate_bitrate,
  285. std::optional<uint32_t> maximum_video_tracks, std::optional<std::string> custom_config,
  286. obs_data_t *dump_stream_to_file_config, size_t main_audio_mixer, std::optional<size_t> vod_track_mixer,
  287. std::optional<bool> use_rtmps, std::optional<QString> extra_canvas)
  288. {
  289. {
  290. const std::lock_guard<std::mutex> current_lock{current_mutex};
  291. const std::lock_guard<std::mutex> current_stream_dump_lock{current_stream_dump_mutex};
  292. if (current || current_stream_dump) {
  293. blog(LOG_WARNING, "Tried to prepare multitrack video output while it's already active");
  294. return;
  295. }
  296. }
  297. restart_on_error = false;
  298. std::optional<GoLiveApi::Config> go_live_config;
  299. std::optional<GoLiveApi::Config> custom;
  300. bool is_custom_config = custom_config.has_value();
  301. auto auto_config_url = MultitrackVideoAutoConfigURL(service);
  302. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  303. auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
  304. if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
  305. multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
  306. }
  307. auto auto_config_url_data = auto_config_url.toUtf8();
  308. std::vector<OBSCanvasAutoRelease> canvases;
  309. canvases.emplace_back(obs_get_main_canvas());
  310. if (extra_canvas) {
  311. obs_canvas_t *canvas = obs_get_canvas_by_uuid(extra_canvas->toUtf8().constData());
  312. if (!canvas) {
  313. throw MultitrackVideoError::critical(QTStr("FailedToStartStream.MissingCanvas"));
  314. }
  315. canvases.emplace_back(canvas);
  316. }
  317. std::string canvasNames;
  318. for (const auto &canvas : canvases) {
  319. if (!canvasNames.empty())
  320. canvasNames += ", ";
  321. canvasNames += obs_canvas_get_name(canvas);
  322. }
  323. DStr vod_track_info_storage;
  324. if (vod_track_mixer.has_value())
  325. dstr_printf(vod_track_info_storage, "Yes (mixer: %zu)", vod_track_mixer.value());
  326. blog(LOG_INFO,
  327. "Preparing enhanced broadcasting stream for:\n"
  328. " custom config: %s\n"
  329. " config url: %s\n"
  330. " settings:\n"
  331. " service: %s\n"
  332. " max aggregate bitrate: %s (%" PRIu32 ")\n"
  333. " max video tracks: %s (%" PRIu32 ")\n"
  334. " custom rtmp url: %s ('%s')\n"
  335. " vod track: %s\n"
  336. " canvases: %s",
  337. is_custom_config ? "Yes" : "No", !auto_config_url.isEmpty() ? auto_config_url_data.constData() : "(null)",
  338. service_name, maximum_aggregate_bitrate.has_value() ? "Set" : "Auto",
  339. maximum_aggregate_bitrate.value_or(0), maximum_video_tracks.has_value() ? "Set" : "Auto",
  340. maximum_video_tracks.value_or(0), rtmp_url.has_value() ? "Yes" : "No",
  341. rtmp_url.has_value() ? rtmp_url->c_str() : "",
  342. vod_track_info_storage->array ? vod_track_info_storage->array : "No", canvasNames.c_str());
  343. const bool custom_config_only = auto_config_url.isEmpty() && MultitrackVideoDeveloperModeEnabled() &&
  344. custom_config.has_value() &&
  345. strcmp(obs_service_get_id(service), "rtmp_custom") == 0;
  346. if (!custom_config_only) {
  347. auto go_live_post = constructGoLivePost(stream_key, maximum_aggregate_bitrate, maximum_video_tracks,
  348. vod_track_mixer.has_value(), canvases);
  349. go_live_config = DownloadGoLiveConfig(parent, auto_config_url, go_live_post, multitrack_video_name);
  350. }
  351. if (custom_config.has_value()) {
  352. GoLiveApi::Config parsed_custom;
  353. try {
  354. parsed_custom = nlohmann::json::parse(*custom_config);
  355. } catch (const nlohmann::json::exception &exception) {
  356. blog(LOG_WARNING, "Failed to parse custom config: %s", exception.what());
  357. throw MultitrackVideoError::critical(QTStr("FailedToStartStream.InvalidCustomConfig"));
  358. }
  359. // copy unique ID from go live request
  360. if (go_live_config.has_value()) {
  361. parsed_custom.meta.config_id = go_live_config->meta.config_id;
  362. blog(LOG_INFO, "Using config_id from go live config with custom config: %s",
  363. parsed_custom.meta.config_id.c_str());
  364. }
  365. nlohmann::json custom_data = parsed_custom;
  366. blog(LOG_INFO, "Using custom go live config: %s", custom_data.dump(4).c_str());
  367. custom.emplace(std::move(parsed_custom));
  368. }
  369. if (go_live_config.has_value()) {
  370. blog(LOG_INFO, "Enhanced broadcasting config_id: '%s'", go_live_config->meta.config_id.c_str());
  371. }
  372. if (!go_live_config && !custom) {
  373. blog(LOG_ERROR, "MultitrackVideoOutput: no config set, this should never happen");
  374. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.NoConfig"));
  375. }
  376. const auto &output_config = custom ? *custom : *go_live_config;
  377. const auto &service_config = go_live_config ? *go_live_config : *custom;
  378. std::vector<OBSEncoderAutoRelease> audio_encoders;
  379. std::shared_ptr<obs_encoder_group_t> video_encoder_group;
  380. auto outputs = SetupOBSOutput(parent, multitrack_video_name, dump_stream_to_file_config, output_config,
  381. audio_encoders, video_encoder_group, audio_encoder_id, main_audio_mixer,
  382. vod_track_mixer, canvases);
  383. auto output = std::move(outputs.output);
  384. auto recording_output = std::move(outputs.recording_output);
  385. if (!output)
  386. throw MultitrackVideoError::warning(
  387. QTStr("FailedToStartStream.FallbackToDefault").arg(multitrack_video_name));
  388. auto multitrack_video_service = create_service(service_config, rtmp_url, stream_key, use_rtmps);
  389. if (!multitrack_video_service)
  390. throw MultitrackVideoError::warning(
  391. QTStr("FailedToStartStream.FallbackToDefault").arg(multitrack_video_name));
  392. obs_output_set_service(output, multitrack_video_service);
  393. // Register the BPM (Broadcast Performance Metrics) callback
  394. obs_output_add_packet_callback(output, bpm_inject, NULL);
  395. // Set callback to prevent reconnection attempts once the stream key has become invalid
  396. static auto reconnect_cb = [](void *param, obs_output_t *, int code) -> bool {
  397. auto _this = static_cast<MultitrackVideoOutput *>(param);
  398. return code != OBS_OUTPUT_INVALID_STREAM || (_this->reconnect_attempts++ < MAX_RECONNECT_ATTEMPTS);
  399. };
  400. obs_output_set_reconnect_callback(output, reconnect_cb, this);
  401. OBSSignal start_streaming;
  402. OBSSignal stop_streaming;
  403. SetupSignalHandlers(false, this, output, start_streaming, stop_streaming);
  404. if (dump_stream_to_file_config && recording_output) {
  405. OBSSignal start_recording;
  406. OBSSignal stop_recording;
  407. SetupSignalHandlers(true, this, recording_output, start_recording, stop_recording);
  408. decltype(audio_encoders) recording_audio_encoders;
  409. recording_audio_encoders.reserve(audio_encoders.size());
  410. for (auto &encoder : audio_encoders) {
  411. recording_audio_encoders.emplace_back(obs_encoder_get_ref(encoder));
  412. }
  413. std::vector<OBSCanvasAutoRelease> recording_canvases(canvases.size());
  414. for (const auto &canvas : canvases) {
  415. recording_canvases.emplace_back(obs_canvas_get_ref(canvas));
  416. }
  417. {
  418. const std::lock_guard current_stream_dump_lock{current_stream_dump_mutex};
  419. current_stream_dump.emplace(OBSOutputObjects{
  420. std::move(recording_output),
  421. video_encoder_group,
  422. std::move(recording_audio_encoders),
  423. nullptr,
  424. std::move(start_recording),
  425. std::move(stop_recording),
  426. std::move(recording_canvases),
  427. });
  428. }
  429. }
  430. const std::lock_guard current_lock{current_mutex};
  431. current.emplace(OBSOutputObjects{
  432. std::move(output),
  433. video_encoder_group,
  434. std::move(audio_encoders),
  435. std::move(multitrack_video_service),
  436. std::move(start_streaming),
  437. std::move(stop_streaming),
  438. std::move(canvases),
  439. });
  440. }
  441. signal_handler_t *MultitrackVideoOutput::StreamingSignalHandler()
  442. {
  443. const std::lock_guard current_lock{current_mutex};
  444. return current.has_value() ? obs_output_get_signal_handler(current->output_) : nullptr;
  445. }
  446. void MultitrackVideoOutput::StartedStreaming()
  447. {
  448. OBSOutputAutoRelease dump_output;
  449. {
  450. const std::lock_guard current_stream_dump_lock{current_stream_dump_mutex};
  451. if (current_stream_dump && current_stream_dump->output_) {
  452. dump_output = obs_output_get_ref(current_stream_dump->output_);
  453. }
  454. }
  455. if (!dump_output)
  456. return;
  457. auto result = obs_output_start(dump_output);
  458. blog(LOG_INFO, "MultitrackVideoOutput: starting recording%s", result ? "" : " failed");
  459. }
  460. void MultitrackVideoOutput::StopStreaming()
  461. {
  462. restart_on_error = false;
  463. OBSOutputAutoRelease current_output;
  464. {
  465. const std::lock_guard current_lock{current_mutex};
  466. if (current && current->output_)
  467. current_output = obs_output_get_ref(current->output_);
  468. }
  469. if (current_output)
  470. obs_output_stop(current_output);
  471. OBSOutputAutoRelease dump_output;
  472. {
  473. const std::lock_guard current_stream_dump_lock{current_stream_dump_mutex};
  474. if (current_stream_dump && current_stream_dump->output_)
  475. dump_output = obs_output_get_ref(current_stream_dump->output_);
  476. }
  477. if (dump_output)
  478. obs_output_stop(dump_output);
  479. }
  480. bool MultitrackVideoOutput::HandleIncompatibleSettings(QWidget *parent, config_t *config, obs_service_t *service,
  481. bool &enableDynBitrate)
  482. {
  483. QString incompatible_settings;
  484. QString where_to_disable;
  485. QString incompatible_settings_list;
  486. size_t num = 1;
  487. auto check_setting = [&](bool setting, const char *name, const char *section) {
  488. if (!setting)
  489. return;
  490. incompatible_settings += QString(" %1. %2\n").arg(num).arg(QTStr(name));
  491. where_to_disable += QString(" %1. [%2 → %3 → %4]\n")
  492. .arg(num)
  493. .arg(QTStr("Settings"))
  494. .arg(QTStr("Basic.Settings.Advanced"))
  495. .arg(QTStr(section));
  496. incompatible_settings_list += QString("%1, ").arg(name);
  497. num += 1;
  498. };
  499. check_setting(enableDynBitrate, "Basic.Settings.Output.DynamicBitrate.Beta", "Basic.Settings.Advanced.Network");
  500. if (incompatible_settings.isEmpty())
  501. return true;
  502. OBSDataAutoRelease service_settings = obs_service_get_settings(service);
  503. QMessageBox mb(parent);
  504. mb.setIcon(QMessageBox::Critical);
  505. mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
  506. mb.setText(QString(QTStr("MultitrackVideo.IncompatibleSettings.Text"))
  507. .arg(obs_data_get_string(service_settings, "multitrack_video_name"))
  508. .arg(incompatible_settings)
  509. .arg(where_to_disable));
  510. auto this_stream = mb.addButton(QTStr("MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming"),
  511. QMessageBox::AcceptRole);
  512. auto all_streams = mb.addButton(QString(QTStr("MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming")),
  513. QMessageBox::AcceptRole);
  514. mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
  515. mb.exec();
  516. const char *action = "cancel";
  517. if (mb.clickedButton() == this_stream) {
  518. action = "DisableAndStartStreaming";
  519. } else if (mb.clickedButton() == all_streams) {
  520. action = "UpdateAndStartStreaming";
  521. }
  522. blog(LOG_INFO,
  523. "MultitrackVideoOutput: attempted to start stream with incompatible"
  524. "settings (%s); action taken: %s",
  525. incompatible_settings_list.toUtf8().constData(), action);
  526. if (mb.clickedButton() == this_stream || mb.clickedButton() == all_streams) {
  527. enableDynBitrate = false;
  528. if (mb.clickedButton() == all_streams) {
  529. config_set_bool(config, "Output", "DynamicBitrate", false);
  530. }
  531. return true;
  532. }
  533. MultitrackVideoOutput::ReleaseOnMainThread(take_current());
  534. MultitrackVideoOutput::ReleaseOnMainThread(take_current_stream_dump());
  535. return false;
  536. }
  537. static bool create_video_encoders(const GoLiveApi::Config &go_live_config,
  538. std::shared_ptr<obs_encoder_group_t> &video_encoder_group, obs_output_t *output,
  539. obs_output_t *recording_output, const std::vector<OBSCanvasAutoRelease> &canvases)
  540. {
  541. DStr video_encoder_name_buffer;
  542. if (go_live_config.encoder_configurations.empty()) {
  543. blog(LOG_WARNING, "MultitrackVideoOutput: Missing video encoder configurations");
  544. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.MissingEncoderConfigs"));
  545. }
  546. std::shared_ptr<obs_encoder_group_t> encoder_group(obs_encoder_group_create(), obs_encoder_group_destroy);
  547. if (!encoder_group)
  548. return false;
  549. auto max_canvas_idx = canvases.size() - 1;
  550. for (size_t i = 0; i < go_live_config.encoder_configurations.size(); i++) {
  551. auto &config = go_live_config.encoder_configurations[i];
  552. if (config.canvas_index > max_canvas_idx) {
  553. blog(LOG_ERROR, "MultitrackVideoOutput: Invalid canvas index: %u", config.canvas_index);
  554. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.InvalidEncoderConfig"));
  555. }
  556. auto &canvas = canvases[config.canvas_index];
  557. auto encoder = create_video_encoder(video_encoder_name_buffer, i, config, canvas);
  558. if (!encoder)
  559. return false;
  560. if (!obs_encoder_set_group(encoder, encoder_group.get()))
  561. return false;
  562. obs_output_set_video_encoder2(output, encoder, i);
  563. if (recording_output)
  564. obs_output_set_video_encoder2(recording_output, encoder, i);
  565. }
  566. video_encoder_group = encoder_group;
  567. return true;
  568. }
  569. static void create_audio_encoders(const GoLiveApi::Config &go_live_config,
  570. std::vector<OBSEncoderAutoRelease> &audio_encoders, obs_output_t *output,
  571. obs_output_t *recording_output, const char *audio_encoder_id, size_t main_audio_mixer,
  572. std::optional<size_t> vod_track_mixer, std::vector<speaker_layout> &speaker_layouts,
  573. speaker_layout &current_layout)
  574. {
  575. speaker_layout speakers = SPEAKERS_UNKNOWN;
  576. obs_audio_info oai = {};
  577. if (obs_get_audio_info(&oai))
  578. speakers = oai.speakers;
  579. current_layout = speakers;
  580. auto sanitize_audio_channels = [&](obs_encoder_t *encoder, uint32_t channels) {
  581. speaker_layout target_speakers = SPEAKERS_UNKNOWN;
  582. for (size_t i = 0; i <= (size_t)SPEAKERS_7POINT1; i++) {
  583. if (get_audio_channels((speaker_layout)i) != channels)
  584. continue;
  585. target_speakers = (speaker_layout)i;
  586. break;
  587. }
  588. if (target_speakers == SPEAKERS_UNKNOWN) {
  589. blog(LOG_WARNING,
  590. "MultitrackVideoOutput: Could not find "
  591. "speaker layout for %" PRIu32 "channels "
  592. "while configuring encoder '%s'",
  593. channels, obs_encoder_get_name(encoder));
  594. return;
  595. }
  596. if (speakers != SPEAKERS_UNKNOWN &&
  597. (channels > get_audio_channels(speakers) || speakers == target_speakers))
  598. return;
  599. auto it = std::find(std::begin(speaker_layouts), std::end(speaker_layouts), target_speakers);
  600. if (it == std::end(speaker_layouts))
  601. speaker_layouts.push_back(target_speakers);
  602. };
  603. using encoder_configs_type = decltype(go_live_config.audio_configurations.live);
  604. DStr encoder_name_buffer;
  605. size_t output_encoder_index = 0;
  606. auto create_encoders = [&](const char *name_prefix, const encoder_configs_type &configs, size_t mixer_idx) {
  607. if (configs.empty()) {
  608. blog(LOG_WARNING, "MultitrackVideoOutput: Missing audio encoder configurations (for '%s')",
  609. name_prefix);
  610. throw MultitrackVideoError::warning(QTStr("FailedToStartStream.MissingEncoderConfigs"));
  611. }
  612. for (size_t i = 0; i < configs.size(); i++) {
  613. dstr_printf(encoder_name_buffer, "%s %zu", name_prefix, i);
  614. OBSDataAutoRelease settings = obs_data_create_from_json(configs[i].settings.dump().c_str());
  615. OBSEncoderAutoRelease audio_encoder =
  616. create_audio_encoder(encoder_name_buffer->array, audio_encoder_id, settings, mixer_idx);
  617. sanitize_audio_channels(audio_encoder, configs[i].channels);
  618. obs_output_set_audio_encoder(output, audio_encoder, output_encoder_index);
  619. if (recording_output)
  620. obs_output_set_audio_encoder(recording_output, audio_encoder, output_encoder_index);
  621. output_encoder_index += 1;
  622. audio_encoders.emplace_back(std::move(audio_encoder));
  623. }
  624. };
  625. create_encoders("multitrack video live audio", go_live_config.audio_configurations.live, main_audio_mixer);
  626. if (!vod_track_mixer.has_value())
  627. return;
  628. // we already check for empty inside of `create_encoders`
  629. encoder_configs_type empty = {};
  630. create_encoders("multitrack video vod audio", go_live_config.audio_configurations.vod.value_or(empty),
  631. *vod_track_mixer);
  632. return;
  633. }
  634. static const char *speaker_layout_to_string(speaker_layout layout)
  635. {
  636. switch (layout) {
  637. case SPEAKERS_MONO:
  638. return "Mono";
  639. case SPEAKERS_2POINT1:
  640. return "2.1";
  641. case SPEAKERS_4POINT0:
  642. return "4.0";
  643. case SPEAKERS_4POINT1:
  644. return "4.1";
  645. case SPEAKERS_5POINT1:
  646. return "5.1";
  647. case SPEAKERS_7POINT1:
  648. return "7.1";
  649. case SPEAKERS_UNKNOWN:
  650. case SPEAKERS_STEREO:
  651. return "Stereo";
  652. }
  653. return "Stereo";
  654. }
  655. static void handle_speaker_layout_issues(QWidget *parent, const QString &multitrack_video_name,
  656. const std::vector<speaker_layout> &requested_layouts, speaker_layout layout)
  657. {
  658. if (requested_layouts.empty())
  659. return;
  660. QString message;
  661. if (requested_layouts.size() == 1) {
  662. message = QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsSingle")
  663. .arg(QTStr(speaker_layout_to_string(requested_layouts.front())));
  664. } else {
  665. message =
  666. QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple").arg(multitrack_video_name);
  667. }
  668. QMetaObject::invokeMethod(
  669. parent,
  670. [&] {
  671. QMessageBox mb(parent);
  672. mb.setIcon(QMessageBox::Critical);
  673. mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
  674. mb.setText(QTStr("MultitrackVideo.IncompatibleSettings.AudioChannels")
  675. .arg(multitrack_video_name)
  676. .arg(QTStr(speaker_layout_to_string(layout)))
  677. .arg(message));
  678. mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
  679. mb.exec();
  680. },
  681. BlockingConnectionTypeFor(parent));
  682. blog(LOG_INFO, "MultitrackVideoOutput: Attempted to start stream with incompatible "
  683. "audio channel setting. Action taken: cancel");
  684. throw MultitrackVideoError::cancel();
  685. }
  686. static OBSOutputs SetupOBSOutput(QWidget *parent, const QString &multitrack_video_name,
  687. obs_data_t *dump_stream_to_file_config, const GoLiveApi::Config &go_live_config,
  688. std::vector<OBSEncoderAutoRelease> &audio_encoders,
  689. std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
  690. const char *audio_encoder_id, size_t main_audio_mixer,
  691. std::optional<size_t> vod_track_mixer,
  692. const std::vector<OBSCanvasAutoRelease> &canvases)
  693. {
  694. auto output = create_output();
  695. OBSOutputAutoRelease recording_output;
  696. if (dump_stream_to_file_config)
  697. recording_output = create_recording_output(dump_stream_to_file_config);
  698. if (!create_video_encoders(go_live_config, video_encoder_group, output, recording_output, canvases))
  699. return {nullptr, nullptr};
  700. std::vector<speaker_layout> requested_speaker_layouts;
  701. speaker_layout current_layout = SPEAKERS_UNKNOWN;
  702. create_audio_encoders(go_live_config, audio_encoders, output, recording_output, audio_encoder_id,
  703. main_audio_mixer, vod_track_mixer, requested_speaker_layouts, current_layout);
  704. handle_speaker_layout_issues(parent, multitrack_video_name, requested_speaker_layouts, current_layout);
  705. return {std::move(output), std::move(recording_output)};
  706. }
  707. void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, obs_output_t *output, OBSSignal &start,
  708. OBSSignal &stop)
  709. {
  710. auto handler = obs_output_get_signal_handler(output);
  711. start.Connect(handler, "start", !recording ? StreamStartHandler : RecordingStartHandler, self);
  712. stop.Connect(handler, "stop", !recording ? StreamStopHandler : RecordingStopHandler, self);
  713. }
  714. std::optional<MultitrackVideoOutput::OBSOutputObjects> MultitrackVideoOutput::take_current()
  715. {
  716. const std::lock_guard<std::mutex> current_lock{current_mutex};
  717. auto val = std::move(current);
  718. current.reset();
  719. return val;
  720. }
  721. std::optional<MultitrackVideoOutput::OBSOutputObjects> MultitrackVideoOutput::take_current_stream_dump()
  722. {
  723. const std::lock_guard<std::mutex> current_stream_dump_lock{current_stream_dump_mutex};
  724. auto val = std::move(current_stream_dump);
  725. current_stream_dump.reset();
  726. return val;
  727. }
  728. void MultitrackVideoOutput::ReleaseOnMainThread(std::optional<OBSOutputObjects> objects)
  729. {
  730. if (!objects.has_value())
  731. return;
  732. QMetaObject::invokeMethod(
  733. QApplication::instance()->thread(), [objects = std::move(objects)] {}, Qt::QueuedConnection);
  734. }
  735. void StreamStartHandler(void *arg, calldata_t *)
  736. {
  737. auto self = static_cast<MultitrackVideoOutput *>(arg);
  738. self->restart_on_error = true;
  739. self->reconnect_attempts = 0;
  740. }
  741. void StreamStopHandler(void *arg, calldata_t *data)
  742. {
  743. auto self = static_cast<MultitrackVideoOutput *>(arg);
  744. OBSOutputAutoRelease stream_dump_output;
  745. {
  746. const std::lock_guard<std::mutex> current_stream_dump_lock{self->current_stream_dump_mutex};
  747. if (self->current_stream_dump && self->current_stream_dump->output_)
  748. stream_dump_output = obs_output_get_ref(self->current_stream_dump->output_);
  749. }
  750. if (stream_dump_output)
  751. obs_output_stop(stream_dump_output);
  752. /* Unregister the BPM (Broadcast Performance Metrics) callback and destroy the allocated metrics data. */
  753. obs_output_remove_packet_callback(static_cast<obs_output_t *>(calldata_ptr(data, "output")), bpm_inject, NULL);
  754. bpm_destroy(static_cast<obs_output_t *>(calldata_ptr(data, "output")));
  755. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
  756. }
  757. void RecordingStartHandler(void * /* arg */, calldata_t * /* data */)
  758. {
  759. blog(LOG_INFO, "MultitrackVideoOutput: recording started");
  760. }
  761. void RecordingStopHandler(void *arg, calldata_t *)
  762. {
  763. auto self = static_cast<MultitrackVideoOutput *>(arg);
  764. blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
  765. MultitrackVideoOutput::ReleaseOnMainThread(self->take_current_stream_dump());
  766. }