multitrack-video-output.cpp 31 KB

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