1
0

multitrack-video-output.cpp 28 KB

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