diff --git a/UI/audio-encoders.cpp b/UI/audio-encoders.cpp index c03501e05d8117..08c4af32b45b31 100644 --- a/UI/audio-encoders.cpp +++ b/UI/audio-encoders.cpp @@ -23,18 +23,17 @@ static const char *EncoderName(const std::string &id) return NullToEmpty(obs_encoder_get_display_name(id.c_str())); } -static map bitrateMap; - -static void HandleIntProperty(obs_property_t *prop, const char *id) +static void HandleIntProperty(obs_property_t *prop, std::vector &bitrates) { const int max_ = obs_property_int_max(prop); const int step = obs_property_int_step(prop); for (int i = obs_property_int_min(prop); i <= max_; i += step) - bitrateMap[i] = id; + bitrates.push_back(i); } -static void HandleListProperty(obs_property_t *prop, const char *id) +static void HandleListProperty(obs_property_t *prop, const char *id, + std::vector &bitrates) { obs_combo_format format = obs_property_list_format(prop); if (format != OBS_COMBO_FORMAT_INT) { @@ -53,7 +52,7 @@ static void HandleListProperty(obs_property_t *prop, const char *id) int bitrate = static_cast(obs_property_list_item_int(prop, i)); - bitrateMap[bitrate] = id; + bitrates.push_back(bitrate); } } @@ -86,7 +85,7 @@ static void HandleSampleRate(obs_property_t *prop, const char *id) obs_property_modified(prop, data.get()); } -static void HandleEncoderProperties(const char *id) +static void HandleEncoderProperties(const char *id, std::vector &bitrates) { auto DestroyProperties = [](obs_properties_t *props) { obs_properties_destroy(props); @@ -112,10 +111,10 @@ static void HandleEncoderProperties(const char *id) obs_property_type type = obs_property_get_type(bitrate); switch (type) { case OBS_PROPERTY_INT: - return HandleIntProperty(bitrate, id); + return HandleIntProperty(bitrate, bitrates); case OBS_PROPERTY_LIST: - return HandleListProperty(bitrate, id); + return HandleListProperty(bitrate, id, bitrates); default: break; @@ -132,10 +131,75 @@ static const char *GetCodec(const char *id) return NullToEmpty(obs_get_encoder_codec(id)); } -static void PopulateBitrateMap() +static std::vector fallbackBitrates; +static map> encoderBitrates; + +static void PopulateBitrateLists() { static once_flag once; + call_once(once, []() { + struct obs_audio_info aoi; + obs_get_audio_info(&aoi); + uint32_t output_channels = get_audio_channels(aoi.speakers); + + /* NOTE: ffmpeg_aac and ffmpeg_opus have the same properties + * their bitrates will also be used as a fallback */ + HandleEncoderProperties("ffmpeg_aac", fallbackBitrates); + + if (fallbackBitrates.empty()) + blog(LOG_ERROR, "Could not enumerate fallback encoder " + "bitrates"); + + ostringstream ss; + for (auto &bitrate : fallbackBitrates) + ss << "\n " << setw(3) << bitrate << " kbit/s:"; + + blog(LOG_DEBUG, "Fallback encoder bitrates:%s", + ss.str().c_str()); + + const char *id = nullptr; + for (size_t i = 0; obs_enum_encoder_types(i, &id); i++) { + if (obs_get_encoder_type(id) != OBS_ENCODER_AUDIO) + continue; + + if (strcmp(id, "ffmpeg_aac") == 0 || + strcmp(id, "ffmpeg_opus") == 0) + continue; + + std::string encoder = id; + + HandleEncoderProperties(id, encoderBitrates[encoder]); + + if (encoderBitrates[encoder].empty()) + blog(LOG_ERROR, + "Could not enumerate %s encoder " + "bitrates", + id); + + ostringstream ss; + for (auto &bitrate : encoderBitrates[encoder]) + ss << "\n " << setw(3) << bitrate + << " kbit/s"; + + blog(LOG_DEBUG, "%s (%s) encoder bitrates:%s", + EncoderName(id), id, ss.str().c_str()); + } + + if (encoderBitrates.empty() && fallbackBitrates.empty()) + blog(LOG_ERROR, "Could not enumerate any audio encoder " + "bitrates"); + }); +} + +static map simpleAACBitrateMap; + +static void PopulateSimpleAACBitrateMap() +{ + PopulateBitrateLists(); + + static once_flag once; + call_once(once, []() { const string encoders[] = { "ffmpeg_aac", @@ -148,7 +212,8 @@ static void PopulateBitrateMap() struct obs_audio_info aoi; obs_get_audio_info(&aoi); - HandleEncoderProperties(fallbackEncoder.c_str()); + for (auto &bitrate : fallbackBitrates) + simpleAACBitrateMap[bitrate] = fallbackEncoder; const char *id = nullptr; for (size_t i = 0; obs_enum_encoder_types(i, &id); i++) { @@ -160,48 +225,115 @@ static void PopulateBitrateMap() end(encoders)) continue; - if (strcmp(GetCodec(id), "AAC") != 0) + if (strcmp(GetCodec(id), "aac") != 0) + continue; + + std::string encoder = id; + if (encoderBitrates[encoder].empty()) continue; - HandleEncoderProperties(id); + for (auto &bitrate : encoderBitrates[encoder]) + simpleAACBitrateMap[bitrate] = encoder; } for (auto &encoder : encoders) { if (encoder == fallbackEncoder) continue; - if (strcmp(GetCodec(encoder.c_str()), "AAC") != 0) + if (strcmp(GetCodec(encoder.c_str()), "aac") != 0) continue; - HandleEncoderProperties(encoder.c_str()); + for (auto &bitrate : encoderBitrates[encoder]) + simpleAACBitrateMap[bitrate] = encoder; } - if (bitrateMap.empty()) { + if (simpleAACBitrateMap.empty()) { blog(LOG_ERROR, "Could not enumerate any AAC encoder " "bitrates"); return; } ostringstream ss; - for (auto &entry : bitrateMap) + for (auto &entry : simpleAACBitrateMap) + ss << "\n " << setw(3) << entry.first + << " kbit/s: '" << EncoderName(entry.second) << "' (" + << entry.second << ')'; + + blog(LOG_DEBUG, "AAC simple encoder bitrate mapping:%s", + ss.str().c_str()); + }); +} + +static map simpleOpusBitrateMap; + +static void PopulateSimpleOpusBitrateMap() +{ + PopulateBitrateLists(); + + static once_flag once; + + call_once(once, []() { + struct obs_audio_info aoi; + obs_get_audio_info(&aoi); + uint32_t output_channels = get_audio_channels(aoi.speakers); + + for (auto &bitrate : fallbackBitrates) + simpleOpusBitrateMap[bitrate] = "ffmpeg_opus"; + + const char *id = nullptr; + for (size_t i = 0; obs_enum_encoder_types(i, &id); i++) { + if (strcmp(GetCodec(id), "opus") != 0) + continue; + + std::string encoder = id; + if (encoderBitrates[encoder].empty()) + continue; + + for (auto &bitrate : encoderBitrates[encoder]) + simpleOpusBitrateMap[bitrate] = encoder; + } + + if (simpleOpusBitrateMap.empty()) { + blog(LOG_ERROR, "Could not enumerate any Opus encoder " + "bitrates"); + return; + } + + ostringstream ss; + for (auto &entry : simpleOpusBitrateMap) ss << "\n " << setw(3) << entry.first << " kbit/s: '" << EncoderName(entry.second) << "' (" << entry.second << ')'; - blog(LOG_DEBUG, "AAC encoder bitrate mapping:%s", + blog(LOG_DEBUG, "Opus simple encoder bitrate mapping:%s", ss.str().c_str()); }); } -const map &GetAACEncoderBitrateMap() +const map &GetSimpleAACEncoderBitrateMap() { - PopulateBitrateMap(); - return bitrateMap; + PopulateSimpleAACBitrateMap(); + return simpleAACBitrateMap; +} + +const map &GetSimpleOpusEncoderBitrateMap() +{ + PopulateSimpleOpusBitrateMap(); + return simpleOpusBitrateMap; +} + +const char *GetSimpleAACEncoderForBitrate(int bitrate) +{ + auto &map_ = GetSimpleAACEncoderBitrateMap(); + auto res = map_.find(bitrate); + if (res == end(map_)) + return NULL; + return res->second.c_str(); } -const char *GetAACEncoderForBitrate(int bitrate) +const char *GetSimpleOpusEncoderForBitrate(int bitrate) { - auto &map_ = GetAACEncoderBitrateMap(); + auto &map_ = GetSimpleOpusEncoderBitrateMap(); auto res = map_.find(bitrate); if (res == end(map_)) return NULL; @@ -210,13 +342,13 @@ const char *GetAACEncoderForBitrate(int bitrate) #define INVALID_BITRATE 10000 -int FindClosestAvailableAACBitrate(int bitrate) +static int FindClosestAvailableSimpleBitrate(int bitrate, + const map &map) { - auto &map_ = GetAACEncoderBitrateMap(); int prev = 0; int next = INVALID_BITRATE; - for (auto val : map_) { + for (auto val : map) { if (next > val.first) { if (val.first == bitrate) return bitrate; @@ -234,3 +366,51 @@ int FindClosestAvailableAACBitrate(int bitrate) return prev; return 192; } + +int FindClosestAvailableSimpleAACBitrate(int bitrate) +{ + return FindClosestAvailableSimpleBitrate( + bitrate, GetSimpleAACEncoderBitrateMap()); +} + +int FindClosestAvailableSimpleOpusBitrate(int bitrate) +{ + return FindClosestAvailableSimpleBitrate( + bitrate, GetSimpleOpusEncoderBitrateMap()); +} + +const std::vector &GetAudioEncoderBitrates(const char *id) +{ + std::string encoder = id; + PopulateBitrateLists(); + if (encoderBitrates[encoder].empty()) + return fallbackBitrates; + return encoderBitrates[encoder]; +} + +int FindClosestAvailableAudioBitrate(const char *id, int bitrate) +{ + int prev = 0; + int next = INVALID_BITRATE; + std::string encoder = id; + + for (auto val : encoderBitrates[encoder].empty() + ? fallbackBitrates + : encoderBitrates[encoder]) { + if (next > val) { + if (val == bitrate) + return bitrate; + + if (val < next && val > bitrate) + next = val; + if (val > prev && val < bitrate) + prev = val; + } + } + + if (next != INVALID_BITRATE) + return next; + if (prev != 0) + return prev; + return 192; +} diff --git a/UI/audio-encoders.hpp b/UI/audio-encoders.hpp index 3bb0596e6f3632..b7fd0ab1085d2a 100644 --- a/UI/audio-encoders.hpp +++ b/UI/audio-encoders.hpp @@ -3,7 +3,15 @@ #include #include +#include -const std::map &GetAACEncoderBitrateMap(); -const char *GetAACEncoderForBitrate(int bitrate); -int FindClosestAvailableAACBitrate(int bitrate); +const std::map &GetSimpleAACEncoderBitrateMap(); +const char *GetSimpleAACEncoderForBitrate(int bitrate); +int FindClosestAvailableSimpleAACBitrate(int bitrate); + +const std::map &GetSimpleOpusEncoderBitrateMap(); +const char *GetSimpleOpusEncoderForBitrate(int bitrate); +int FindClosestAvailableSimpleOpusBitrate(int bitrate); + +const std::vector &GetAudioEncoderBitrates(const char *id); +int FindClosestAvailableAudioBitrate(const char *id, int bitrate); diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index cc32edf6ebbe47..0d91518d5e2e2e 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -218,6 +218,7 @@ Basic.AutoConfig.TestPage="Final Results" Basic.AutoConfig.TestPage.SubTitle.Testing="The program is now executing a set of tests to estimate the ideal settings" Basic.AutoConfig.TestPage.SubTitle.Complete="Testing complete" Basic.AutoConfig.TestPage.TestingBandwidth="Performing bandwidth test, this may take a few minutes..." +Basic.AutoConfig.TestPage.TestingBandwidth.NoOutput="No output for the protocol of this service was found" Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="Connecting to: %1..." Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="Failed to connect to any servers, please check your internet connection and try again." Basic.AutoConfig.TestPage.TestingBandwidth.Server="Testing bandwidth for: %1" @@ -941,7 +942,8 @@ Basic.Settings.Output.Format.HLS="HLS (.m3u8 + .ts)" Basic.Settings.Output.Format.fMP4="Fragmented MP4 (.mp4)" Basic.Settings.Output.Format.fMOV="Fragmented MOV (.mov)" Basic.Settings.Output.Format.TT="Fragmented MP4/MOV writes the recording in chunks and does not require the same finalization as traditional MP4/MOV files.\nThis ensures the file remains playable even if writing to disk is interrupted, for example, as a result of a BSOD or power loss.\n\nThis may not be compatible with all players and editors. Use File → Remux Recordings to convert the file into a more compatible format if necessary." -Basic.Settings.Output.Encoder="Encoder" +Basic.Settings.Output.Encoder.Video="Video Encoder" +Basic.Settings.Output.Encoder.Audio="Audio Encoder" Basic.Settings.Output.SelectDirectory="Select Recording Directory" Basic.Settings.Output.SelectFile="Select Recording File" Basic.Settings.Output.DynamicBitrate="Dynamically change bitrate to manage congestion" @@ -992,6 +994,7 @@ Basic.Settings.Output.Warn.EnforceResolutionFPS.Resolution="Resolution: %1" Basic.Settings.Output.Warn.EnforceResolutionFPS.FPS="FPS: %1" Basic.Settings.Output.Warn.ServiceCodecCompatibility.Title="Incompatible Encoder" Basic.Settings.Output.Warn.ServiceCodecCompatibility.Msg="The streaming service \"%1\" does not support the encoder \"%2\". The encoder will be changed to \"%3\".\n\nDo you want to continue?" +Basic.Settings.Output.Warn.ServiceCodecCompatibility.Msg2="The streaming service \"%1\" does not support encoders \"%2\" and \"%3\". These encoders will be changed to \"%4\" and \"%5\".\n\nDo you want to continue?" Basic.Settings.Output.VideoBitrate="Video Bitrate" Basic.Settings.Output.AudioBitrate="Audio Bitrate" Basic.Settings.Output.Reconnect="Automatically Reconnect" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index bb894fb204c7c2..7eb79970070c94 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -1731,7 +1731,7 @@ - Basic.Settings.Output.Encoder + Basic.Settings.Output.Encoder.Video simpleOutRecEncoder @@ -1780,6 +1780,19 @@ + + + + Basic.Settings.Output.Encoder.Audio + + + simpleOutStrAEncoder + + + + + + @@ -1902,7 +1915,7 @@ - Basic.Settings.Output.Encoder + Basic.Settings.Output.Encoder.Video simpleOutRecEncoder @@ -1913,6 +1926,19 @@ + + + Basic.Settings.Output.Encoder.Audio + + + simpleOutRecAEncoder + + + + + + + Basic.Settings.Output.CustomMuxerSettings @@ -1922,10 +1948,10 @@ - + - + Basic.Settings.Output.UseReplayBuffer @@ -2268,16 +2294,26 @@ - + - Basic.Settings.Output.Encoder + Basic.Settings.Output.Encoder.Audio - + + + + Basic.Settings.Output.Encoder.Video + + + + + + + @@ -2293,7 +2329,7 @@ - + false @@ -2856,15 +2892,15 @@ - - + + - Basic.Settings.Output.Encoder + Basic.Settings.Output.Encoder.Audio - + 0 @@ -2874,6 +2910,23 @@ + + + Basic.Settings.Output.Encoder.Video + + + + + + + + 0 + 0 + + + + + @@ -2889,7 +2942,7 @@ - + @@ -2923,14 +2976,14 @@ - + Basic.Settings.Output.CustomMuxerSettings - + @@ -2940,7 +2993,7 @@ - + @@ -2956,7 +3009,7 @@ - + false @@ -2978,14 +3031,14 @@ - + Basic.Settings.Output.SplitFile.Time - + min @@ -3001,14 +3054,14 @@ - + Basic.Settings.Output.SplitFile.Size - + MB diff --git a/UI/ui-validation.cpp b/UI/ui-validation.cpp index e441cbb6a49125..e45fd3f74ad410 100644 --- a/UI/ui-validation.cpp +++ b/UI/ui-validation.cpp @@ -59,26 +59,24 @@ bool UIValidation::NoSourcesConfirmation(QWidget *parent) StreamSettingsAction UIValidation::StreamSettingsConfirmation(QWidget *parent, OBSService service) { - // Custom services can user API key in URL or user/pass combo. - // So only check there is a URL + if (obs_service_can_try_to_connect(service)) + return StreamSettingsAction::ContinueStream; + char const *serviceType = obs_service_get_type(service); - bool isCustomUrlService = (strcmp(serviceType, "rtmp_custom") == 0); + bool isCustomService = (strcmp(serviceType, "rtmp_custom") == 0); - char const *streamUrl = obs_service_get_url(service); - char const *streamKey = obs_service_get_key(service); + char const *streamUrl = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + char const *streamKey = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_STREAM_KEY); - bool hasStreamUrl = (streamUrl != NULL && streamUrl[0] != '\0'); - bool hasStreamKey = ((streamKey != NULL && streamKey[0] != '\0') || - isCustomUrlService); - - if (hasStreamUrl && hasStreamKey) - return StreamSettingsAction::ContinueStream; + bool streamUrlMissing = !(streamUrl != NULL && streamUrl[0] != '\0'); + bool streamKeyMissing = !(streamKey != NULL && streamKey[0] != '\0'); QString msg; - - if (!hasStreamUrl && !hasStreamKey) { + if (!isCustomService && streamUrlMissing && streamKeyMissing) { msg = QTStr("Basic.Settings.Stream.MissingUrlAndApiKey"); - } else if (!hasStreamKey) { + } else if (!isCustomService && streamKeyMissing) { msg = QTStr("Basic.Settings.Stream.MissingStreamKey"); } else { msg = QTStr("Basic.Settings.Stream.MissingUrl"); diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index ceb2e1cfbaa026..c6d7a694267623 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -88,6 +88,7 @@ class TestMode { #define SUBTITLE_TESTING TEST_STR("Subtitle.Testing") #define SUBTITLE_COMPLETE TEST_STR("Subtitle.Complete") #define TEST_BW TEST_STR("TestingBandwidth") +#define TEST_BW_NO_OUTPUT TEST_STR("TestingBandwidth.NoOutput") #define TEST_BW_CONNECTING TEST_STR("TestingBandwidth.Connecting") #define TEST_BW_CONNECT_FAIL TEST_STR("TestingBandwidth.ConnectFailed") #define TEST_BW_SERVER TEST_STR("TestingBandwidth.Server") @@ -155,6 +156,23 @@ static inline void string_depad_key(string &key) const char *FindAudioEncoderFromCodec(const char *type); +static inline bool can_use_output(const char *prot, const char *output, + const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || + (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + void AutoConfigTestPage::TestBandwidthThread() { bool connected = false; @@ -268,9 +286,37 @@ void AutoConfigTestPage::TestBandwidthThread() /* -----------------------------------*/ /* create output */ - const char *output_type = obs_service_get_output_type(service); - if (!output_type) - output_type = "rtmp_output"; + /* Check if the service has a preferred output type */ + const char *output_type = + obs_service_get_preferred_output_type(service); + if (!output_type || + (obs_get_output_flags(output_type) & OBS_OUTPUT_SERVICE) == 0) { + /* Otherwise, prefer first-party output types */ + const char *protocol = obs_service_get_protocol(service); + + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + output_type = "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", + "HLS")) { + output_type = "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", + "SRT", "RIST")) { + output_type = "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + if (!output_type) + obs_enum_output_types_with_protocol( + protocol, &output_type, return_first_id); + + /* If none, fail */ + if (!output_type) { + QMetaObject::invokeMethod( + this, "Failure", + Q_ARG(QString, QTStr(TEST_BW_NO_OUTPUT))); + return; + } + } OBSOutputAutoRelease output = obs_output_create(output_type, "test_stream", nullptr, nullptr); diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 8a78d3240a45a7..bcd861831a8ba8 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -186,20 +186,34 @@ static void OBSStopVirtualCam(void *data, calldata_t *params) /* ------------------------------------------------------------------------ */ -static bool CreateAACEncoder(OBSEncoder &res, string &id, int bitrate, - const char *name, size_t idx) +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, + const char *name, size_t idx) { - const char *id_ = GetAACEncoderForBitrate(bitrate); + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); if (!id_) { - id.clear(); res = nullptr; return false; } - if (id == id_) + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, + const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } - id = id_; res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); if (res) { @@ -210,6 +224,73 @@ static bool CreateAACEncoder(OBSEncoder &res, string &id, int bitrate, return false; } +static inline bool can_use_output(const char *prot, const char *output, + const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || + (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", + obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", + protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, + "The output '%s' is not registered, fallback to another one", + output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", + "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, + "No output compatible with the service '%s' is registered", + obs_service_get_id(service)); + + return nullptr; +} + /* ------------------------------------------------------------------------ */ inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) @@ -352,16 +433,12 @@ void BasicOutputHandler::DestroyVirtualCamView() /* ------------------------------------------------------------------------ */ struct SimpleOutput : BasicOutputHandler { - OBSEncoder aacStreaming; + OBSEncoder audioStreaming; OBSEncoder videoStreaming; - OBSEncoder aacRecording; - OBSEncoder aacArchive; + OBSEncoder audioRecording; + OBSEncoder audioArchive; OBSEncoder videoRecording; - string aacRecEncID; - string aacStreamEncID; - string aacArchiveEncID; - string videoEncoder; string videoQuality; bool usingRecordingPreset = false; @@ -494,6 +571,8 @@ void SimpleOutput::LoadRecordingPreset() config_get_string(main->Config(), "SimpleOutput", "RecQuality"); const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string( + main->Config(), "SimpleOutput", "RecAudioEncoder"); videoEncoder = encoder; videoQuality = quality; @@ -501,7 +580,7 @@ void SimpleOutput::LoadRecordingPreset() if (strcmp(quality, "Stream") == 0) { videoRecording = videoStreaming; - aacRecording = aacStreaming; + audioRecording = audioStreaming; usingRecordingPreset = false; return; @@ -519,28 +598,56 @@ void SimpleOutput::LoadRecordingPreset() LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); usingRecordingPreset = true; - if (!CreateAACEncoder(aacRecording, aacRecEncID, 192, - "simple_aac_recording", 0)) - throw "Failed to create aac recording encoder " + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder( + audioRecording, 192, "simple_opus_recording", + 0); + else + success = CreateSimpleAACEncoder( + audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " "(simple output)"; } } -#define SIMPLE_ARCHIVE_NAME "simple_archive_aac" +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) { const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string( + main->Config(), "SimpleOutput", "StreamAudioEncoder"); LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); - if (!CreateAACEncoder(aacStreaming, aacStreamEncID, GetAudioBitrate(), - "simple_aac", 0)) - throw "Failed to create aac streaming encoder (simple output)"; - if (!CreateAACEncoder(aacArchive, aacArchiveEncID, GetAudioBitrate(), - SIMPLE_ARCHIVE_NAME, 1)) - throw "Failed to create aac arhive encoder (simple output)"; + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder( + audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder( + audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, + GetAudioBitrate(), + SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, + GetAudioBitrate(), + SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; LoadRecordingPreset(); @@ -595,16 +702,21 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) int SimpleOutput::GetAudioBitrate() const { + const char *audio_encoder = config_get_string( + main->Config(), "SimpleOutput", "StreamAudioEncoder"); int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); - return FindClosestAvailableAACBitrate(bitrate); + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); } void SimpleOutput::Update() { OBSDataAutoRelease videoSettings = obs_data_create(); - OBSDataAutoRelease aacSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); @@ -665,15 +777,15 @@ void SimpleOutput::Update() if (advanced) obs_data_set_string(videoSettings, "x264opts", custom); - obs_data_set_string(aacSettings, "rate_control", "CBR"); - obs_data_set_int(aacSettings, "bitrate", audioBitrate); + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); obs_service_apply_encoder_settings(main->GetService(), videoSettings, - aacSettings); + audioSettings); if (!enforceBitrate) { obs_data_set_int(videoSettings, "bitrate", videoBitrate); - obs_data_set_int(aacSettings, "bitrate", audioBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); } video_t *video = obs_get_video(); @@ -691,8 +803,8 @@ void SimpleOutput::Update() } obs_encoder_update(videoStreaming, videoSettings); - obs_encoder_update(aacStreaming, aacSettings); - obs_encoder_update(aacArchive, aacSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); } void SimpleOutput::UpdateRecordingAudioSettings() @@ -701,7 +813,7 @@ void SimpleOutput::UpdateRecordingAudioSettings() obs_data_set_int(settings, "bitrate", 192); obs_data_set_string(settings, "rate_control", "CBR"); - obs_encoder_update(aacRecording, settings); + obs_encoder_update(audioRecording, settings); } #define CROSS_DIST_CUTOFF 2000.0 @@ -873,8 +985,8 @@ inline void SimpleOutput::SetupOutputs() { SimpleOutput::Update(); obs_encoder_set_video(videoStreaming, obs_get_video()); - obs_encoder_set_audio(aacStreaming, obs_get_audio()); - obs_encoder_set_audio(aacArchive, obs_get_audio()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); if (usingRecordingPreset) { if (ffmpegOutput) { @@ -882,7 +994,7 @@ inline void SimpleOutput::SetupOutputs() obs_get_audio()); } else { obs_encoder_set_video(videoRecording, obs_get_video()); - obs_encoder_set_audio(aacRecording, obs_get_audio()); + obs_encoder_set_audio(audioRecording, obs_get_audio()); } } } @@ -913,18 +1025,9 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) /* --------------------- */ - const char *type = obs_service_get_output_type(service); - if (!type) { - type = "rtmp_output"; - const char *url = obs_service_get_url(service); - if (url != NULL && - strncmp(url, FTL_PROTOCOL, strlen(FTL_PROTOCOL)) == 0) { - type = "ftl_output"; - } else if (url != NULL && strncmp(url, RTMP_PROTOCOL, - strlen(RTMP_PROTOCOL)) != 0) { - type = "ffmpeg_mpegts_muxer"; - } - } + const char *type = GetStreamOutputType(service); + if (!type) + return false; /* XXX: this is messy and disgusting and should be refactored */ if (outputType != type) { @@ -960,41 +1063,11 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) bool isEncoded = obs_output_get_flags(streamOutput) & OBS_OUTPUT_ENCODED; - if (isEncoded) { - const char *codec = - obs_output_get_supported_audio_codecs( - streamOutput); - if (!codec) { - blog(LOG_WARNING, "Failed to load audio codec"); - return false; - } - - if (strcmp(codec, "aac") != 0) { - const char *id = - FindAudioEncoderFromCodec(codec); - int audioBitrate = GetAudioBitrate(); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", - audioBitrate); - - aacStreaming = obs_audio_encoder_create( - id, "alt_audio_enc", nullptr, 0, - nullptr); - obs_encoder_release(aacStreaming); - if (!aacStreaming) - return false; - - obs_encoder_update(aacStreaming, settings); - obs_encoder_set_audio(aacStreaming, - obs_get_audio()); - } - } - outputType = type; } obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, aacStreaming, 0); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); obs_output_set_service(streamOutput, service); return true; } @@ -1037,7 +1110,7 @@ void SimpleOutput::SetupVodTrack(obs_service_t *service) enable = advanced && enable && ServiceSupportsVodTrack(name); if (enable) - obs_output_set_audio_encoder(streamOutput, aacArchive, 1); + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); else clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); } @@ -1120,11 +1193,11 @@ void SimpleOutput::UpdateRecording() if (!ffmpegOutput) { obs_output_set_video_encoder(fileOutput, videoRecording); - obs_output_set_audio_encoder(fileOutput, aacRecording, 0); + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); } if (replayBuffer) { obs_output_set_video_encoder(replayBuffer, videoRecording); - obs_output_set_audio_encoder(replayBuffer, aacRecording, 0); + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); } recordingConfigured = true; @@ -1290,17 +1363,16 @@ bool SimpleOutput::ReplayBufferActive() const struct AdvancedOutput : BasicOutputHandler { OBSEncoder streamAudioEnc; OBSEncoder streamArchiveEnc; - OBSEncoder aacTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; OBSEncoder videoStreaming; OBSEncoder videoRecording; bool ffmpegOutput; bool ffmpegRecording; bool useStreamEncoder; + bool useStreamAudioEncoder; bool usesBitrate = false; - string aacEncoderID[MAX_AUDIO_MIXES]; - AdvancedOutput(OBSBasic *main_); inline void UpdateStreamSettings(); @@ -1314,7 +1386,7 @@ struct AdvancedOutput : BasicOutputHandler { inline void SetupRecording(); inline void SetupFFmpeg(); void SetupOutputs() override; - int GetAudioBitrate(size_t i) const; + int GetAudioBitrate(size_t i, const char *id) const; virtual bool SetupStreaming(obs_service_t *service) override; virtual bool StartStreaming(obs_service_t *service) override; @@ -1358,7 +1430,7 @@ static void ApplyEncoderDefaults(OBSData &settings, settings = std::move(dataRet); } -#define ADV_ARCHIVE_NAME "adv_archive_aac" +#define ADV_ARCHIVE_NAME "adv_archive_audio" #ifdef __APPLE__ static void translate_macvth264_encoder(const char *&encoder) @@ -1377,8 +1449,12 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) config_get_string(main->Config(), "AdvOut", "RecType"); const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = + config_get_string(main->Config(), "AdvOut", "AudioEncoder"); const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = + config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); #ifdef __APPLE__ translate_macvth264_encoder(streamEncoder); translate_macvth264_encoder(recordEncoder); @@ -1389,6 +1465,7 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); @@ -1468,30 +1545,43 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) astrcmpi(rate_control, "ABR") == 0; for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - char name[9]; - snprintf(name, sizeof(name), "adv_aac%d", i); + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create( + useStreamAudioEncoder ? streamAudioEncoder + : recAudioEncoder, + name, nullptr, i, nullptr); - if (!CreateAACEncoder(aacTrack[i], aacEncoderID[i], - GetAudioBitrate(i), name, i)) + if (!recordTrack[i]) { throw "Failed to create audio encoder " "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); } std::string id; int streamTrack = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - if (!CreateAACEncoder(streamAudioEnc, id, GetAudioBitrate(streamTrack), - "adv_stream_aac", streamTrack)) + streamAudioEnc = obs_audio_encoder_create(streamAudioEncoder, + "adv_stream_audio", nullptr, + streamTrack, nullptr); + if (!streamAudioEnc) throw "Failed to create streaming audio encoder " "(advanced output)"; + obs_encoder_release(streamAudioEnc); id = ""; int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; - if (!CreateAACEncoder(streamArchiveEnc, id, GetAudioBitrate(vodTrack), - ADV_ARCHIVE_NAME, vodTrack)) + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, + ADV_ARCHIVE_NAME, nullptr, + streamTrack, nullptr); + if (!streamArchiveEnc) throw "Failed to create archive audio encoder " "(advanced output)"; + obs_encoder_release(streamArchiveEnc); startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); @@ -1659,21 +1749,22 @@ inline void AdvancedOutput::SetupRecording() if (!flv) { for (int i = 0; i < MAX_AUDIO_MIXES; i++) { if ((tracks & (1 << i)) != 0) { - obs_output_set_audio_encoder(fileOutput, - aacTrack[i], idx); + obs_output_set_audio_encoder( + fileOutput, recordTrack[i], idx); if (replayBuffer) obs_output_set_audio_encoder( - replayBuffer, aacTrack[i], idx); + replayBuffer, recordTrack[i], + idx); idx++; } } } else if (flv && tracks != 0) { - obs_output_set_audio_encoder(fileOutput, aacTrack[tracks - 1], - idx); + obs_output_set_audio_encoder(fileOutput, + recordTrack[tracks - 1], idx); if (replayBuffer) - obs_output_set_audio_encoder(replayBuffer, - aacTrack[tracks - 1], idx); + obs_output_set_audio_encoder( + replayBuffer, recordTrack[tracks - 1], idx); } // Use fragmented MOV/MP4 if user has not already specified custom movflags @@ -1776,13 +1867,12 @@ inline void AdvancedOutput::UpdateAudioSettings() config_get_int(main->Config(), "AdvOut", "TrackIndex"); int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = + config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = + config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { - settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i)); - } - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { string cfg_name = "Track"; cfg_name += to_string((int)i + 1); @@ -1792,13 +1882,19 @@ inline void AdvancedOutput::UpdateAudioSettings() string def_name = "Track"; def_name += to_string((int)i + 1); - SetEncoderName(aacTrack[i], name, def_name.c_str()); + SetEncoderName(recordTrack[i], name, def_name.c_str()); } for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", + GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); - obs_encoder_update(aacTrack[i], settings[i]); + obs_data_set_int(settings[i], "bitrate", + GetAudioBitrate(i, audioEncoder)); if (track == streamTrackIndex || track == vodTrackIndex) { if (applyServiceSettings) { @@ -1827,7 +1923,7 @@ void AdvancedOutput::SetupOutputs() if (videoRecording) obs_encoder_set_video(videoRecording, obs_get_video()); for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) - obs_encoder_set_audio(aacTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); @@ -1839,14 +1935,14 @@ void AdvancedOutput::SetupOutputs() SetupRecording(); } -int AdvancedOutput::GetAudioBitrate(size_t i) const +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const { static const char *names[] = { "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", }; int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAACBitrate(bitrate); + return FindClosestAvailableAudioBitrate(id, bitrate); } inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) @@ -1898,18 +1994,9 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) /* --------------------- */ - const char *type = obs_service_get_output_type(service); - if (!type) { - type = "rtmp_output"; - const char *url = obs_service_get_url(service); - if (url != NULL && - strncmp(url, FTL_PROTOCOL, strlen(FTL_PROTOCOL)) == 0) { - type = "ftl_output"; - } else if (url != NULL && strncmp(url, RTMP_PROTOCOL, - strlen(RTMP_PROTOCOL)) != 0) { - type = "ffmpeg_mpegts_muxer"; - } - } + const char *type = GetStreamOutputType(service); + if (!type) + return false; /* XXX: this is messy and disgusting and should be refactored */ if (outputType != type) { @@ -1945,37 +2032,6 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) bool isEncoded = obs_output_get_flags(streamOutput) & OBS_OUTPUT_ENCODED; - if (isEncoded) { - const char *codec = - obs_output_get_supported_audio_codecs( - streamOutput); - if (!codec) { - blog(LOG_WARNING, "Failed to load audio codec"); - return false; - } - - if (strcmp(codec, "aac") != 0) { - OBSDataAutoRelease settings = - obs_encoder_get_settings( - streamAudioEnc); - - const char *id = - FindAudioEncoderFromCodec(codec); - - streamAudioEnc = obs_audio_encoder_create( - id, "alt_audio_enc", nullptr, - streamTrack - 1, nullptr); - - if (!streamAudioEnc) - return false; - - obs_encoder_release(streamAudioEnc); - obs_encoder_update(streamAudioEnc, settings); - obs_encoder_set_audio(streamAudioEnc, - obs_get_audio()); - } - } - outputType = type; } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 1a3465ba45b4ca..b7eb69e597a145 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1485,6 +1485,10 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_int(basicConfig, "SimpleOutput", "RecRBSize", 512); config_set_default_string(basicConfig, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(basicConfig, "SimpleOutput", + "StreamAudioEncoder", "aac"); + config_set_default_string(basicConfig, "SimpleOutput", + "RecAudioEncoder", "aac"); config_set_default_bool(basicConfig, "AdvOut", "ApplyServiceSettings", true); @@ -1501,6 +1505,8 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_bool(basicConfig, "AdvOut", "RecUseRescale", false); config_set_default_uint(basicConfig, "AdvOut", "RecTracks", (1 << 0)); config_set_default_string(basicConfig, "AdvOut", "RecEncoder", "none"); + config_set_default_string(basicConfig, "AdvOut", "RecAudioEncoder", + "none"); config_set_default_uint(basicConfig, "AdvOut", "FLVTrack", 1); config_set_default_bool(basicConfig, "AdvOut", "FFOutputToFile", true); @@ -1629,6 +1635,15 @@ void OBSBasic::InitBasicConfigDefaults2() useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(basicConfig, "AdvOut", "AudioEncoder", + aac_default); + if (update_nvenc_presets(basicConfig)) config_save_safe(basicConfig, "tmp", nullptr); } diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index f691baefce4f47..004d8ab50dbe1d 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -78,29 +78,10 @@ void OBSBasicSettings::InitStreamPage() ui->twitchAddonDropdown->addItem( QTStr("Basic.Settings.Stream.TTVAddon.Both")); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateServerList())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateKeyLink())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateVodTrackSetting())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateServiceRecommendations())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateResFPSLimits())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateKeyLink())); connect(ui->ignoreRecommended, SIGNAL(clicked(bool)), this, SLOT(DisplayEnforceWarning(bool))); connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this, SLOT(UpdateResFPSLimits())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateMoreInfoLink())); - - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateAdvNetworkGroup())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateAdvNetworkGroup())); } void OBSBasicSettings::LoadStream1Settings() @@ -118,11 +99,13 @@ void OBSBasicSettings::LoadStream1Settings() const char *service = obs_data_get_string(settings, "service"); const char *server = obs_data_get_string(settings, "server"); const char *key = obs_data_get_string(settings, "key"); + protocol = QT_UTF8(obs_service_get_protocol(service_obj)); if (strcmp(type, "rtmp_custom") == 0) { ui->service->setCurrentIndex(0); ui->customServer->setText(server); lastServiceIdx = 0; + lastCustomServer = ui->customServer->text(); bool use_auth = obs_data_get_bool(settings, "use_auth"); const char *username = @@ -458,13 +441,46 @@ void OBSBasicSettings::UseStreamKeyAdvClicked() ui->streamKeyWidget->setVisible(true); } -void OBSBasicSettings::on_service_currentIndexChanged(int) +void OBSBasicSettings::on_service_currentIndexChanged(int idx) { - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; - if (showMore) + if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) { + LoadServices(true); + ui->service->showPopup(); return; + } + + ServiceChanged(); + + UpdateMoreInfoLink(); + UpdateServerList(); + UpdateKeyLink(); + UpdateServiceRecommendations(); + + UpdateVodTrackSetting(); + + protocol = FindProtocol(); + UpdateAdvNetworkGroup(); + + if (ServiceSupportsCodecCheck() && UpdateResFPSLimits()) { + lastServiceIdx = idx; + if (idx == 0) + lastCustomServer = ui->customServer->text(); + } +} + +void OBSBasicSettings::on_customServer_textChanged(const QString &) +{ + UpdateKeyLink(); + + protocol = FindProtocol(); + UpdateAdvNetworkGroup(); + + if (ServiceSupportsCodecCheck()) + lastCustomServer = ui->customServer->text(); +} +void OBSBasicSettings::ServiceChanged() +{ std::string service = QT_TO_UTF8(ui->service->currentText()); bool custom = IsCustomService(); @@ -515,20 +531,49 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) } } -void OBSBasicSettings::UpdateServerList() +QString OBSBasicSettings::FindProtocol() { - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; + if (IsCustomService() && !ui->customServer->text().isEmpty()) { + + QString server = ui->customServer->text(); + + if (server.startsWith("rtmps://")) + return QString("RTMPS"); + + if (server.startsWith("ftl://")) + return QString("FTL"); + + if (server.startsWith("srt://")) + return QString("SRT"); + + if (server.startsWith("rist://")) + return QString("RIST"); - if (showMore) { - LoadServices(true); - ui->service->showPopup(); - return; } else { - lastService = serviceName; + obs_properties_t *props = + obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "service", + QT_TO_UTF8(ui->service->currentText())); + obs_property_modified(services, settings); + + obs_properties_destroy(props); + + return QT_UTF8(obs_data_get_string(settings, "protocol")); } + return QString("RTMP"); +} + +void OBSBasicSettings::UpdateServerList() +{ + QString serviceName = ui->service->currentText(); + + lastService = serviceName; + obs_properties_t *props = obs_get_service_properties("rtmp_common"); obs_property_t *services = obs_properties_get(props, "service"); @@ -985,17 +1030,14 @@ extern void set_closest_res(int &cx, int &cy, * which as of this writing, and hopefully for the foreseeable future, there is * only one. */ -void OBSBasicSettings::UpdateResFPSLimits() +bool OBSBasicSettings::UpdateResFPSLimits() { if (loading) - return; - - if (!ServiceSupportsCodecCheck()) - return; + return false; int idx = ui->service->currentIndex(); if (idx == -1) - return; + return false; bool ignoreRecommended = ui->ignoreRecommended->isChecked(); BPtr res_list; @@ -1073,8 +1115,7 @@ void OBSBasicSettings::UpdateResFPSLimits() ui->ignoreRecommended->setProperty("changed", true); stream1Changed = true; EnableApplyButton(true); - UpdateResFPSLimits(); - return; + return UpdateResFPSLimits(); } QMessageBox::StandardButton button; @@ -1106,7 +1147,7 @@ void OBSBasicSettings::UpdateResFPSLimits() "setChecked", Qt::QueuedConnection, Q_ARG(bool, true)); - return; + return false; } } @@ -1187,24 +1228,8 @@ void OBSBasicSettings::UpdateResFPSLimits() /* ------------------------------------ */ lastIgnoreRecommended = (int)ignoreRecommended; - lastServiceIdx = idx; -} - -bool OBSBasicSettings::IsServiceOutputHasNetworkFeatures() -{ - if (IsCustomService()) - return ui->customServer->text().startsWith("rtmp"); - - OBSServiceAutoRelease service = SpawnTempService(); - const char *output = obs_service_get_output_type(service); - - if (!output) - return true; - if (strcmp(output, "rtmp_output") == 0) - return true; - - return false; + return true; } static bool service_supports_codec(const char **codecs, const char *codec) @@ -1234,17 +1259,19 @@ static inline bool service_supports_encoder(const char **codecs, return service_supports_codec(codecs, codec); } -bool OBSBasicSettings::ServiceAndCodecCompatible() +static bool return_first_id(void *data, const char *id) { - if (IsCustomService()) - return true; - if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) - return true; + const char **output = (const char **)data; + + *output = id; + return false; +} +bool OBSBasicSettings::ServiceAndVCodecCompatible() +{ bool simple = (ui->outputMode->currentIndex() == 0); + bool ret; - OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_video_codecs(service); const char *codec; if (simple) { @@ -1257,7 +1284,67 @@ bool OBSBasicSettings::ServiceAndCodecCompatible() codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); } - return service_supports_codec(codecs, codec); + OBSService service = SpawnTempService(); + const char **codecs = obs_service_get_supported_video_codecs(service); + + if (!codecs || IsCustomService()) { + const char *output; + char **output_codecs; + + obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), + &output, return_first_id); + + output_codecs = strlist_split( + obs_get_output_supported_video_codecs(output), ';', + false); + + ret = service_supports_codec((const char **)output_codecs, + codec); + + strlist_free(output_codecs); + } else { + ret = service_supports_codec(codecs, codec); + } + + return ret; +} + +bool OBSBasicSettings::ServiceAndACodecCompatible() +{ + bool simple = (ui->outputMode->currentIndex() == 0); + bool ret; + + QString codec; + + if (simple) { + codec = ui->simpleOutStrAEncoder->currentData().toString(); + } else { + QString encoder = ui->advOutAEncoder->currentData().toString(); + codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); + } + + OBSService service = SpawnTempService(); + const char **codecs = obs_service_get_supported_audio_codecs(service); + + if (!codecs || IsCustomService()) { + const char *output; + char **output_codecs; + + obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), + &output, return_first_id); + output_codecs = strlist_split( + obs_get_output_supported_audio_codecs(output), ';', + false); + + ret = service_supports_codec((const char **)output_codecs, + QT_TO_UTF8(codec)); + + strlist_free(output_codecs); + } else { + ret = service_supports_codec(codecs, QT_TO_UTF8(codec)); + } + + return ret; } /* we really need a way to find fallbacks in a less hardcoded way. maybe. */ @@ -1272,6 +1359,22 @@ static QString get_adv_fallback(const QString &enc) return "obs_x264"; } +static QString get_adv_audio_fallback(const QString &enc) +{ + const char *codec = obs_get_encoder_codec(QT_TO_UTF8(enc)); + + if (strcmp(codec, "aac") == 0) + return "ffmpeg_opus"; + + QString aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + return aac_default; +} + static QString get_simple_fallback(const QString &enc) { if (enc == SIMPLE_ENCODER_NVENC_HEVC) @@ -1287,15 +1390,24 @@ static QString get_simple_fallback(const QString &enc) bool OBSBasicSettings::ServiceSupportsCodecCheck() { - if (ServiceAndCodecCompatible()) { - if (lastServiceIdx != ui->service->currentIndex()) + if (loading) + return false; + + bool vcodec_compat = ServiceAndVCodecCompatible(); + bool acodec_compat = ServiceAndACodecCompatible(); + + if (vcodec_compat && acodec_compat) { + if (lastServiceIdx != ui->service->currentIndex() || + IsCustomService()) ResetEncoders(true); return true; } QString service = ui->service->currentText(); - QString cur_name; - QString fb_name; + QString cur_video_name; + QString fb_video_name; + QString cur_audio_name; + QString fb_audio_name; bool simple = (ui->outputMode->currentIndex() == 0); /* ------------------------------------------------- */ @@ -1309,27 +1421,60 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() int cur_idx = ui->simpleOutStrEncoder->findData(cur_enc); int fb_idx = ui->simpleOutStrEncoder->findData(fb_enc); - cur_name = ui->simpleOutStrEncoder->itemText(cur_idx); - fb_name = ui->simpleOutStrEncoder->itemText(fb_idx); + cur_video_name = ui->simpleOutStrEncoder->itemText(cur_idx); + fb_video_name = ui->simpleOutStrEncoder->itemText(fb_idx); + + cur_enc = ui->simpleOutStrAEncoder->currentData().toString(); + fb_enc = (cur_enc == "opus") ? "aac" : "opus"; + + cur_audio_name = ui->simpleOutStrAEncoder->itemText( + ui->simpleOutStrAEncoder->findData(cur_enc)); + fb_audio_name = ui->simpleOutStrAEncoder->itemText( + ui->simpleOutStrAEncoder->findData(fb_enc)); } else { QString cur_enc = ui->advOutEncoder->currentData().toString(); QString fb_enc = get_adv_fallback(cur_enc); - cur_name = obs_encoder_get_display_name(QT_TO_UTF8(cur_enc)); - fb_name = obs_encoder_get_display_name(QT_TO_UTF8(fb_enc)); + cur_video_name = + obs_encoder_get_display_name(QT_TO_UTF8(cur_enc)); + fb_video_name = + obs_encoder_get_display_name(QT_TO_UTF8(fb_enc)); + + cur_enc = ui->advOutAEncoder->currentData().toString(); + fb_enc = get_adv_audio_fallback(cur_enc); + + cur_audio_name = + obs_encoder_get_display_name(QT_TO_UTF8(cur_enc)); + fb_audio_name = + obs_encoder_get_display_name(QT_TO_UTF8(fb_enc)); } #define WARNING_VAL(x) \ QTStr("Basic.Settings.Output.Warn.ServiceCodecCompatibility." x) - QString msg = WARNING_VAL("Msg").arg(service, cur_name, fb_name); + QString msg = WARNING_VAL("Msg").arg( + service, vcodec_compat ? cur_audio_name : cur_video_name, + vcodec_compat ? fb_audio_name : fb_video_name); + if (!vcodec_compat && !acodec_compat) + msg = WARNING_VAL("Msg2").arg(service, cur_video_name, + cur_audio_name, fb_video_name, + fb_audio_name); + auto button = OBSMessageBox::question(this, WARNING_VAL("Title"), msg); #undef WARNING_VAL if (button == QMessageBox::No) { - QMetaObject::invokeMethod(ui->service, "setCurrentIndex", - Qt::QueuedConnection, - Q_ARG(int, lastServiceIdx)); + if (lastServiceIdx == 0 && + lastServiceIdx == ui->service->currentIndex()) + QMetaObject::invokeMethod(ui->customServer, "setText", + Qt::QueuedConnection, + Q_ARG(QString, + lastCustomServer)); + else + QMetaObject::invokeMethod(ui->service, + "setCurrentIndex", + Qt::QueuedConnection, + Q_ARG(int, lastServiceIdx)); return false; } @@ -1342,25 +1487,60 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() void OBSBasicSettings::ResetEncoders(bool streamOnly) { - QString lastAdvEnc = ui->advOutEncoder->currentData().toString(); - QString lastEnc = ui->simpleOutStrEncoder->currentData().toString(); + QString lastAdvVideoEnc = ui->advOutEncoder->currentData().toString(); + QString lastVideoEnc = + ui->simpleOutStrEncoder->currentData().toString(); + QString lastAdvAudioEnc = ui->advOutAEncoder->currentData().toString(); + QString lastAudioEnc = + ui->simpleOutStrAEncoder->currentData().toString(); OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_video_codecs(service); + const char **vcodecs = obs_service_get_supported_video_codecs(service); + const char **acodecs = obs_service_get_supported_audio_codecs(service); const char *type; + BPtr output_vcodecs; + BPtr output_acodecs; size_t idx = 0; + if (!vcodecs || IsCustomService()) { + const char *output; + + obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), + &output, return_first_id); + output_vcodecs = strlist_split( + obs_get_output_supported_video_codecs(output), ';', + false); + vcodecs = (const char **)output_vcodecs.Get(); + } + + if (!acodecs || IsCustomService()) { + const char *output; + + obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), + &output, return_first_id); + output_acodecs = strlist_split( + obs_get_output_supported_audio_codecs(output), ';', + false); + acodecs = (const char **)output_acodecs.Get(); + } + QSignalBlocker s1(ui->simpleOutStrEncoder); QSignalBlocker s2(ui->advOutEncoder); + QSignalBlocker s3(ui->simpleOutStrAEncoder); + QSignalBlocker s4(ui->advOutAEncoder); /* ------------------------------------------------- */ /* clear encoder lists */ ui->simpleOutStrEncoder->clear(); ui->advOutEncoder->clear(); + ui->simpleOutStrAEncoder->clear(); + ui->advOutAEncoder->clear(); if (!streamOnly) { ui->advOutRecEncoder->clear(); ui->advOutRecEncoder->addItem(TEXT_USE_STREAM_ENC, "none"); + ui->advOutRecAEncoder->clear(); + ui->advOutRecAEncoder->addItem(TEXT_USE_STREAM_ENC, "none"); } /* ------------------------------------------------- */ @@ -1371,33 +1551,25 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) const char *codec = obs_get_encoder_codec(type); uint32_t caps = obs_get_encoder_caps(type); - if (obs_get_encoder_type(type) != OBS_ENCODER_VIDEO) - continue; + QString qName = QT_UTF8(name); + QString qType = QT_UTF8(type); - const char *streaming_codecs[] = { - "h264", -#ifdef ENABLE_HEVC - "hevc", -#endif - }; + if (obs_get_encoder_type(type) == OBS_ENCODER_VIDEO) { + if ((caps & ENCODER_HIDE_FLAGS) != 0) + continue; - bool is_streaming_codec = false; - for (const char *test_codec : streaming_codecs) { - if (strcmp(codec, test_codec) == 0) { - is_streaming_codec = true; - break; - } + if (service_supports_codec(vcodecs, codec)) + ui->advOutEncoder->addItem(qName, qType); + if (!streamOnly) + ui->advOutRecEncoder->addItem(qName, qType); } - if ((caps & ENCODER_HIDE_FLAGS) != 0) - continue; - - QString qName = QT_UTF8(name); - QString qType = QT_UTF8(type); - if (is_streaming_codec && service_supports_codec(codecs, codec)) - ui->advOutEncoder->addItem(qName, qType); - if (!streamOnly) - ui->advOutRecEncoder->addItem(qName, qType); + if (obs_get_encoder_type(type) == OBS_ENCODER_AUDIO) { + if (service_supports_codec(acodecs, codec)) + ui->advOutAEncoder->addItem(qName, qType); + if (!streamOnly) + ui->advOutRecAEncoder->addItem(qName, qType); + } } /* ------------------------------------------------- */ @@ -1407,40 +1579,40 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) ui->simpleOutStrEncoder->addItem(ENCODER_STR("Software"), QString(SIMPLE_ENCODER_X264)); - if (service_supports_encoder(codecs, "obs_qsv11")) + if (service_supports_encoder(vcodecs, "obs_qsv11")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.QSV.H264"), QString(SIMPLE_ENCODER_QSV)); - if (service_supports_encoder(codecs, "obs_qsv11_av1")) + if (service_supports_encoder(vcodecs, "obs_qsv11_av1")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.QSV.AV1"), QString(SIMPLE_ENCODER_QSV_AV1)); - if (service_supports_encoder(codecs, "ffmpeg_nvenc")) + if (service_supports_encoder(vcodecs, "ffmpeg_nvenc")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.NVENC.H264"), QString(SIMPLE_ENCODER_NVENC)); - if (service_supports_encoder(codecs, "jim_av1_nvenc")) + if (service_supports_encoder(vcodecs, "jim_av1_nvenc")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.NVENC.AV1"), QString(SIMPLE_ENCODER_NVENC_AV1)); #ifdef ENABLE_HEVC - if (service_supports_encoder(codecs, "h265_texture_amf")) + if (service_supports_encoder(vcodecs, "h265_texture_amf")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.AMD.HEVC"), QString(SIMPLE_ENCODER_AMD_HEVC)); - if (service_supports_encoder(codecs, "ffmpeg_hevc_nvenc")) + if (service_supports_encoder(vcodecs, "ffmpeg_hevc_nvenc")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.NVENC.HEVC"), QString(SIMPLE_ENCODER_NVENC_HEVC)); #endif - if (service_supports_encoder(codecs, "h264_texture_amf")) + if (service_supports_encoder(vcodecs, "h264_texture_amf")) ui->simpleOutStrEncoder->addItem( ENCODER_STR("Hardware.AMD.H264"), QString(SIMPLE_ENCODER_AMD)); /* Preprocessor guard required for the macOS version check */ #ifdef __APPLE__ if (service_supports_encoder( - codecs, "com.apple.videotoolbox.videoencoder.ave.avc") + vcodecs, "com.apple.videotoolbox.videoencoder.ave.avc") #ifndef __aarch64__ && os_get_emulation_status() == true #endif @@ -1453,7 +1625,7 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) } #ifdef ENABLE_HEVC if (service_supports_encoder( - codecs, "com.apple.videotoolbox.videoencoder.ave.hevc") + vcodecs, "com.apple.videotoolbox.videoencoder.ave.hevc") #ifndef __aarch64__ && os_get_emulation_status() == true #endif @@ -1466,36 +1638,72 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) } #endif #endif + if (service_supports_encoder(acodecs, "CoreAudio_AAC") || + service_supports_encoder(acodecs, "libfdk_aac") || + service_supports_encoder(acodecs, "ffmpeg_aac")) + ui->simpleOutStrAEncoder->addItem("AAC", "aac"); + if (service_supports_encoder(acodecs, "ffmpeg_opus")) + ui->simpleOutStrAEncoder->addItem("Opus", "opus"); #undef ENCODER_STR /* ------------------------------------------------- */ /* Find fallback encoders */ - if (!lastAdvEnc.isEmpty()) { - int idx = ui->advOutEncoder->findData(lastAdvEnc); + if (!lastAdvVideoEnc.isEmpty()) { + int idx = ui->advOutEncoder->findData(lastAdvVideoEnc); if (idx == -1) { - lastAdvEnc = get_adv_fallback(lastAdvEnc); + lastAdvVideoEnc = get_adv_fallback(lastAdvVideoEnc); ui->advOutEncoder->setProperty("changed", QVariant(true)); OutputsChanged(); } - idx = ui->advOutEncoder->findData(lastAdvEnc); + idx = ui->advOutEncoder->findData(lastAdvVideoEnc); s2.unblock(); ui->advOutEncoder->setCurrentIndex(idx); } - if (!lastEnc.isEmpty()) { - int idx = ui->simpleOutStrEncoder->findData(lastEnc); + if (!lastAdvAudioEnc.isEmpty()) { + int idx = ui->advOutAEncoder->findData(lastAdvAudioEnc); + if (idx == -1) { + lastAdvAudioEnc = + get_adv_audio_fallback(lastAdvAudioEnc); + ui->advOutAEncoder->setProperty("changed", + QVariant(true)); + OutputsChanged(); + } + + idx = ui->advOutAEncoder->findData(lastAdvAudioEnc); + s4.unblock(); + ui->advOutAEncoder->setCurrentIndex(idx); + } + + if (!lastVideoEnc.isEmpty()) { + int idx = ui->simpleOutStrEncoder->findData(lastVideoEnc); if (idx == -1) { - lastEnc = get_simple_fallback(lastEnc); + lastVideoEnc = get_simple_fallback(lastVideoEnc); ui->simpleOutStrEncoder->setProperty("changed", QVariant(true)); OutputsChanged(); } - idx = ui->simpleOutStrEncoder->findData(lastEnc); + idx = ui->simpleOutStrEncoder->findData(lastVideoEnc); s1.unblock(); ui->simpleOutStrEncoder->setCurrentIndex(idx); } + + if (!lastAudioEnc.isEmpty()) { + int idx = ui->simpleOutStrAEncoder->findData(lastAudioEnc); + if (idx == -1) { + lastAudioEnc = (lastAudioEnc == "opus") ? "aac" + : "opus"; + ui->simpleOutStrAEncoder->setProperty("changed", + QVariant(true)); + OutputsChanged(); + } + + idx = ui->simpleOutStrAEncoder->findData(lastAudioEnc); + s3.unblock(); + ui->simpleOutStrAEncoder->setCurrentIndex(idx); + } } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 2a5ac8d88287d6..84abe98badb5c9 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -260,9 +260,40 @@ static CodecDesc GetDefaultCodecDesc(const ff_format_desc *formatDesc, id); } -static void PopulateAACBitrates(initializer_list boxes) +#define INVALID_BITRATE 10000 +static int FindClosestAvailableAudioBitrate(QComboBox *box, int bitrate) { - auto &bitrateMap = GetAACEncoderBitrateMap(); + QList bitrates; + int prev = 0; + int next = INVALID_BITRATE; + + for (int i = 0; i < box->count(); i++) + bitrates << box->itemText(i).toInt(); + + for (int val : bitrates) { + if (next > val) { + if (val == bitrate) + return bitrate; + + if (val < next && val > bitrate) + next = val; + if (val > prev && val < bitrate) + prev = val; + } + } + + if (next != INVALID_BITRATE) + return next; + if (prev != 0) + return prev; + return 192; +} +#undef INVALID_BITRATE + +static void PopulateSimpleBitrates(QComboBox *box, bool opus) +{ + auto &bitrateMap = opus ? GetSimpleOpusEncoderBitrateMap() + : GetSimpleAACEncoderBitrateMap(); if (bitrateMap.empty()) return; @@ -272,17 +303,52 @@ static void PopulateAACBitrates(initializer_list boxes) QString::number(entry.first), obs_encoder_get_display_name(entry.second.c_str())); + QString currentBitrate = box->currentText(); + box->clear(); + + for (auto &pair : pairs) { + box->addItem(pair.first); + box->setItemData(box->count() - 1, pair.second, + Qt::ToolTipRole); + } + + if (box->findData(currentBitrate) == -1) { + int bitrate = FindClosestAvailableAudioBitrate( + box, currentBitrate.toInt()); + box->setCurrentText(QString::number(bitrate)); + } else + box->setCurrentText(currentBitrate); +} + +static void PopulateAdvancedBitrates(initializer_list boxes, + const char *stream_id, const char *rec_id) +{ + auto &streamBitrates = GetAudioEncoderBitrates(stream_id); + auto &recBitrates = GetAudioEncoderBitrates(rec_id); + if (streamBitrates.empty() || recBitrates.empty()) + return; + + QList streamBitratesList; + for (auto &bitrate : streamBitrates) + streamBitratesList << bitrate; + for (auto box : boxes) { - QString currentText = box->currentText(); + QString currentBitrate = box->currentText(); box->clear(); - for (auto &pair : pairs) { - box->addItem(pair.first); - box->setItemData(box->count() - 1, pair.second, - Qt::ToolTipRole); + for (auto &bitrate : recBitrates) { + if (streamBitratesList.indexOf(bitrate) == -1) + continue; + + box->addItem(QString::number(bitrate)); } - box->setCurrentText(currentText); + if (box->findData(currentBitrate) == -1) { + int bitrate = FindClosestAvailableAudioBitrate( + box, currentBitrate.toInt()); + box->setCurrentText(QString::number(bitrate)); + } else + box->setCurrentText(currentBitrate); } } @@ -362,11 +428,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) main->EnableOutputs(false); - PopulateAACBitrates({ui->simpleOutputABitrate, ui->advOutTrack1Bitrate, - ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, - ui->advOutTrack6Bitrate}); - ui->listWidget->setAttribute(Qt::WA_MacShowFocusRect, false); /* clang-format off */ @@ -424,17 +485,20 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->simpleOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputVBitrate, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutStrEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutStrAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputABitrate, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutAdvanced, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutPreset, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutCustom, EDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutRecQuality, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->simpleOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleReplayBuf, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutUseRescale, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); @@ -448,6 +512,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->advOutNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecFormat, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutRecAEncoder, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecUseRescale, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); @@ -919,6 +984,13 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) connect(ui->useStreamKeyAdv, SIGNAL(clicked()), this, SLOT(UseStreamKeyAdvClicked())); + connect(ui->simpleOutStrAEncoder, SIGNAL(currentIndexChanged(int)), + this, SLOT(SimpleStreamAudioEncoderChanged)); + connect(ui->advOutAEncoder, SIGNAL(currentIndexChanged(int)), this, + SLOT(AdvAudioEncodersChanged())); + connect(ui->advOutRecAEncoder, SIGNAL(currentIndexChanged(int)), this, + SLOT(AdvAudioEncodersChanged())); + UpdateAudioWarnings(); UpdateAdvNetworkGroup(); } @@ -1821,6 +1893,8 @@ void OBSBasicSettings::LoadSimpleOutputSettings() config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); const char *streamEnc = config_get_string( main->Config(), "SimpleOutput", "StreamEncoder"); + const char *streamAudioEnc = config_get_string( + main->Config(), "SimpleOutput", "StreamAudioEncoder"); int audioBitrate = config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); bool advanced = @@ -1841,6 +1915,8 @@ void OBSBasicSettings::LoadSimpleOutputSettings() config_get_string(main->Config(), "SimpleOutput", "RecQuality"); const char *recEnc = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *recAudioEnc = config_get_string( + main->Config(), "SimpleOutput", "RecAudioEncoder"); const char *muxCustom = config_get_string( main->Config(), "SimpleOutput", "MuxerCustom"); bool replayBuf = @@ -1856,7 +1932,10 @@ void OBSBasicSettings::LoadSimpleOutputSettings() curAMDPreset = amdPreset; curAMDAV1Preset = amdAV1Preset; - audioBitrate = FindClosestAvailableAACBitrate(audioBitrate); + bool isOpus = strcmp(streamAudioEnc, "opus") == 0; + audioBitrate = + isOpus ? FindClosestAvailableSimpleOpusBitrate(audioBitrate) + : FindClosestAvailableSimpleAACBitrate(audioBitrate); ui->simpleOutputPath->setText(path); ui->simpleNoSpace->setChecked(noSpace); @@ -1865,6 +1944,8 @@ void OBSBasicSettings::LoadSimpleOutputSettings() int idx = ui->simpleOutRecFormat->findData(format); ui->simpleOutRecFormat->setCurrentIndex(idx); + PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); + const char *speakers = config_get_string(main->Config(), "Audio", "ChannelSetup"); @@ -1888,11 +1969,21 @@ void OBSBasicSettings::LoadSimpleOutputSettings() idx = 0; ui->simpleOutStrEncoder->setCurrentIndex(idx); + idx = ui->simpleOutStrAEncoder->findData(QString(streamAudioEnc)); + if (idx == -1) + idx = 0; + ui->simpleOutStrAEncoder->setCurrentIndex(idx); + idx = ui->simpleOutRecEncoder->findData(QString(recEnc)); if (idx == -1) idx = 0; ui->simpleOutRecEncoder->setCurrentIndex(idx); + idx = ui->simpleOutRecAEncoder->findData(QString(recAudioEnc)); + if (idx == -1) + idx = 0; + ui->simpleOutRecAEncoder->setCurrentIndex(idx); + ui->simpleOutMuxCustom->setText(muxCustom); ui->simpleReplayBuf->setChecked(replayBuf); @@ -2237,12 +2328,31 @@ void OBSBasicSettings::LoadAdvOutputAudioSettings() const char *name6 = config_get_string(main->Config(), "AdvOut", "Track6Name"); - track1Bitrate = FindClosestAvailableAACBitrate(track1Bitrate); - track2Bitrate = FindClosestAvailableAACBitrate(track2Bitrate); - track3Bitrate = FindClosestAvailableAACBitrate(track3Bitrate); - track4Bitrate = FindClosestAvailableAACBitrate(track4Bitrate); - track5Bitrate = FindClosestAvailableAACBitrate(track5Bitrate); - track6Bitrate = FindClosestAvailableAACBitrate(track6Bitrate); + const char *encoder_id = + config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *rec_encoder_id = + config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + PopulateAdvancedBitrates( + {ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, + ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, + ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + encoder_id, + strcmp(rec_encoder_id, "none") != 0 ? rec_encoder_id + : encoder_id); + + track1Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack1Bitrate, track1Bitrate); + track2Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack2Bitrate, track2Bitrate); + track3Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack3Bitrate, track3Bitrate); + track4Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack4Bitrate, track4Bitrate); + track5Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack5Bitrate, track5Bitrate); + track6Bitrate = FindClosestAvailableAudioBitrate( + ui->advOutTrack6Bitrate, track6Bitrate); // restrict list of bitrates when multichannel is OFF const char *speakers = @@ -2292,8 +2402,15 @@ void OBSBasicSettings::LoadOutputSettings() LoadSimpleOutputSettings(); LoadAdvOutputStreamingSettings(); LoadAdvOutputStreamingEncoderProperties(); + + const char *type = + config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + SetComboByValue(ui->advOutAEncoder, type); + LoadAdvOutputRecordingSettings(); LoadAdvOutputRecordingEncoderProperties(); + type = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + SetComboByValue(ui->advOutRecAEncoder, type); LoadAdvOutputFFmpegSettings(); LoadAdvOutputAudioSettings(); @@ -2302,6 +2419,8 @@ void OBSBasicSettings::LoadOutputSettings() ui->outputModeLabel->setEnabled(false); ui->simpleOutStrEncoderLabel->setEnabled(false); ui->simpleOutStrEncoder->setEnabled(false); + ui->simpleOutStrAEncoderLabel->setEnabled(false); + ui->simpleOutStrAEncoder->setEnabled(false); ui->simpleRecordingGroupBox->setEnabled(false); ui->replayBufferGroupBox->setEnabled(false); ui->advOutTopContainer->setEnabled(false); @@ -3634,6 +3753,8 @@ void OBSBasicSettings::SaveOutputSettings() SaveSpinBox(ui->simpleOutputVBitrate, "SimpleOutput", "VBitrate"); SaveComboData(ui->simpleOutStrEncoder, "SimpleOutput", "StreamEncoder"); + SaveComboData(ui->simpleOutStrAEncoder, "SimpleOutput", + "StreamAudioEncoder"); SaveCombo(ui->simpleOutputABitrate, "SimpleOutput", "ABitrate"); SaveEdit(ui->simpleOutputPath, "SimpleOutput", "FilePath"); SaveCheckBox(ui->simpleNoSpace, "SimpleOutput", "FileNameWithoutSpace"); @@ -3643,6 +3764,8 @@ void OBSBasicSettings::SaveOutputSettings() SaveEdit(ui->simpleOutCustom, "SimpleOutput", "x264Settings"); SaveComboData(ui->simpleOutRecQuality, "SimpleOutput", "RecQuality"); SaveComboData(ui->simpleOutRecEncoder, "SimpleOutput", "RecEncoder"); + SaveComboData(ui->simpleOutRecAEncoder, "SimpleOutput", + "RecAudioEncoder"); SaveEdit(ui->simpleOutMuxCustom, "SimpleOutput", "MuxerCustom"); SaveCheckBox(ui->simpleReplayBuf, "SimpleOutput", "RecRB"); SaveSpinBox(ui->simpleRBSecMax, "SimpleOutput", "RecRBTime"); @@ -3651,6 +3774,7 @@ void OBSBasicSettings::SaveOutputSettings() curAdvStreamEncoder = GetComboData(ui->advOutEncoder); SaveComboData(ui->advOutEncoder, "AdvOut", "Encoder"); + SaveComboData(ui->advOutAEncoder, "AdvOut", "AudioEncoder"); SaveCheckBox(ui->advOutUseRescale, "AdvOut", "Rescale"); SaveCombo(ui->advOutRescale, "AdvOut", "RescaleRes"); SaveTrackIndex(main->Config(), "AdvOut", "TrackIndex", ui->advOutTrack1, @@ -3666,6 +3790,7 @@ void OBSBasicSettings::SaveOutputSettings() SaveCheckBox(ui->advOutNoSpace, "AdvOut", "RecFileNameWithoutSpace"); SaveComboData(ui->advOutRecFormat, "AdvOut", "RecFormat"); SaveComboData(ui->advOutRecEncoder, "AdvOut", "RecEncoder"); + SaveComboData(ui->advOutRecAEncoder, "AdvOut", "RecAudioEncoder"); SaveCheckBox(ui->advOutRecUseRescale, "AdvOut", "RecRescale"); SaveCombo(ui->advOutRecRescale, "AdvOut", "RecRescaleRes"); SaveEdit(ui->advOutMuxCustom, "AdvOut", "RecMuxerCustom"); @@ -4404,16 +4529,26 @@ void OBSBasicSettings::SpeakerLayoutChanged(int idx) QString speakerLayoutQstr = ui->channelSetup->itemText(idx); std::string speakerLayout = QT_TO_UTF8(speakerLayoutQstr); bool surround = IsSurround(speakerLayout.c_str()); + bool isOpus = ui->simpleOutStrAEncoder->currentData().toString() == + "opus"; if (surround) { /* * Display all bitrates */ - PopulateAACBitrates( - {ui->simpleOutputABitrate, ui->advOutTrack1Bitrate, - ui->advOutTrack2Bitrate, ui->advOutTrack3Bitrate, - ui->advOutTrack4Bitrate, ui->advOutTrack5Bitrate, - ui->advOutTrack6Bitrate}); + PopulateSimpleBitrates(ui->simpleOutputABitrate, isOpus); + + const char *encoder_id = QT_TO_UTF8( + ui->advOutAEncoder->currentData().toString()); + QString rec_encoder_id = + ui->advOutRecAEncoder->currentData().toString(); + PopulateAdvancedBitrates( + {ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, + ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, + ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + encoder_id, + rec_encoder_id == "none" ? encoder_id + : QT_TO_UTF8(rec_encoder_id)); } else { /* * Reset audio bitrate for simple and adv mode, update list of @@ -4465,7 +4600,8 @@ void RestrictResetBitrates(initializer_list boxes, int maxbitrate) { for (auto box : boxes) { int idx = box->currentIndex(); - int max_bitrate = FindClosestAvailableAACBitrate(maxbitrate); + int max_bitrate = + FindClosestAvailableAudioBitrate(box, maxbitrate); int count = box->count(); int max_idx = box->findText( QT_UTF8(std::to_string(max_bitrate).c_str())); @@ -4474,8 +4610,8 @@ void RestrictResetBitrates(initializer_list boxes, int maxbitrate) box->removeItem(i); if (idx > max_idx) { - int default_bitrate = - FindClosestAvailableAACBitrate(maxbitrate / 2); + int default_bitrate = FindClosestAvailableAudioBitrate( + box, maxbitrate / 2); int default_idx = box->findText(QT_UTF8( std::to_string(default_bitrate).c_str())); @@ -4945,6 +5081,12 @@ void OBSBasicSettings::FillSimpleRecordingValues() QString(SIMPLE_ENCODER_APPLE_HEVC)); #endif + if (EncoderAvailable("CoreAudio_AAC") || + EncoderAvailable("libfdk_aac") || EncoderAvailable("ffmpeg_aac")) + ui->simpleOutRecAEncoder->addItem("AAC", "aac"); + if (EncoderAvailable("ffmpeg_opus")) + ui->simpleOutRecAEncoder->addItem("Opus", "opus"); + #undef ADD_QUALITY #undef ENCODER_STR } @@ -4975,6 +5117,8 @@ void OBSBasicSettings::SimpleRecordingQualityChanged() bool showEncoder = !streamQuality && !losslessQuality; ui->simpleOutRecEncoder->setVisible(showEncoder); ui->simpleOutRecEncoderLabel->setVisible(showEncoder); + ui->simpleOutRecAEncoder->setVisible(showEncoder); + ui->simpleOutRecAEncoderLabel->setVisible(showEncoder); ui->simpleOutRecFormat->setVisible(!losslessQuality); ui->simpleOutRecFormatLabel->setVisible(!losslessQuality); @@ -5621,7 +5765,7 @@ void OBSBasicSettings::RecreateOutputResolutionWidget() void OBSBasicSettings::UpdateAdvNetworkGroup() { - bool enabled = IsServiceOutputHasNetworkFeatures(); + bool enabled = protocol.contains("RTMP"); ui->advNetworkDisabled->setVisible(!enabled); @@ -5633,3 +5777,39 @@ void OBSBasicSettings::UpdateAdvNetworkGroup() ui->enableLowLatencyMode->setVisible(enabled); #endif } + +void OBSBasicSettings::SimpleStreamAudioEncoderChanged() +{ + PopulateSimpleBitrates( + ui->simpleOutputABitrate, + ui->simpleOutStrAEncoder->currentData().toString() == "opus"); + + if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) + return; + + RestrictResetBitrates({ui->simpleOutputABitrate}, 320); +} + +void OBSBasicSettings::AdvAudioEncodersChanged() +{ + QString streamEncoder = ui->advOutAEncoder->currentData().toString(); + QString recEncoder = ui->advOutRecAEncoder->currentData().toString(); + + if (recEncoder == "none") + recEncoder = streamEncoder; + + PopulateAdvancedBitrates( + {ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, + ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, + ui->advOutTrack5Bitrate, ui->advOutTrack6Bitrate}, + QT_TO_UTF8(streamEncoder), QT_TO_UTF8(recEncoder)); + + if (IsSurround(QT_TO_UTF8(ui->channelSetup->currentText()))) + return; + + RestrictResetBitrates({ui->advOutTrack1Bitrate, ui->advOutTrack2Bitrate, + ui->advOutTrack3Bitrate, ui->advOutTrack4Bitrate, + ui->advOutTrack5Bitrate, + ui->advOutTrack6Bitrate}, + 320); +} diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 51f00336f8e98c..d3442db3c18ac9 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -261,16 +261,23 @@ class OBSBasicSettings : public QDialog { void OnOAuthStreamKeyConnected(); void OnAuthConnected(); QString lastService; + QString protocol; + QString lastCustomServer; int prevLangIndex; bool prevBrowserAccel; -private slots: + + void ServiceChanged(); + QString FindProtocol(); void UpdateServerList(); void UpdateKeyLink(); void UpdateVodTrackSetting(); void UpdateServiceRecommendations(); - void RecreateOutputResolutionWidget(); - void UpdateResFPSLimits(); void UpdateMoreInfoLink(); + void UpdateAdvNetworkGroup(); + +private slots: + void RecreateOutputResolutionWidget(); + bool UpdateResFPSLimits(); void DisplayEnforceWarning(bool checked); void on_show_clicked(); void on_authPwShow_clicked(); @@ -370,9 +377,8 @@ private slots: OBSService GetStream1Service(); - bool IsServiceOutputHasNetworkFeatures(); - - bool ServiceAndCodecCompatible(); + bool ServiceAndVCodecCompatible(); + bool ServiceAndACodecCompatible(); bool ServiceSupportsCodecCheck(); private slots: @@ -382,6 +388,7 @@ private slots: void on_buttonBox_clicked(QAbstractButton *button); void on_service_currentIndexChanged(int idx); + void on_customServer_textChanged(const QString &text); void on_simpleOutputBrowse_clicked(); void on_advOutRecPathBrowse_clicked(); void on_advOutFFPathBrowse_clicked(); @@ -435,8 +442,6 @@ private slots: void UpdateStreamDelayEstimate(); - void UpdateAdvNetworkGroup(); - void UpdateAutomaticReplayBufferCheckboxes(); void AdvOutSplitFileChanged(); @@ -464,6 +469,9 @@ private slots: void UseStreamKeyAdvClicked(); + void SimpleStreamAudioEncoderChanged(); + void AdvAudioEncodersChanged(); + protected: virtual void closeEvent(QCloseEvent *event) override; void reject() override; diff --git a/docs/sphinx/reference-outputs.rst b/docs/sphinx/reference-outputs.rst index 0cbe9f98865d2c..ba60f70ebbaf83 100644 --- a/docs/sphinx/reference-outputs.rst +++ b/docs/sphinx/reference-outputs.rst @@ -234,7 +234,15 @@ Output Definition Structure (obs_output_info) This variable specifies which codecs are supported by an encoded output, separated by semicolon. - (Optional, though recommended) + Required if **OBS_OUTPUT_SERVICE** flag is set, otherwise + recommended. + +.. member:: const char *obs_output_info.protocols + + This variable specifies which protocols are supported by an output, + separated by semicolon. + + Required only if **OBS_OUTPUT_SERVICE** flag is set. .. _output_signal_handler_reference: @@ -671,7 +679,9 @@ General Output Functions --------------------- .. function:: const char *obs_output_get_supported_video_codecs(const obs_output_t *output) + const char *obs_get_output_supported_video_codecs(const char *id) const char *obs_output_get_supported_audio_codecs(const obs_output_t *output) + const char *obs_get_output_supported_video_codecs(const char *id) :return: Supported video/audio codecs of an encoded output, separated by semicolon @@ -685,6 +695,40 @@ General Output Functions --------------------- +.. function:: const char *obs_output_get_protocols(const obs_output_t *output) + + :return: Supported protocols, separated by semicolon. Always NULL if the + output is not **OBS_OUTPUT_SERVICE**. + +--------------------- + +.. function:: bool obs_is_output_protocol_registered(const char *protocol) + + Check if one of the registered output use the given protocol. + + :return: A boolean showing if an output with the given + protocol is registered + +--------------------- + +.. function:: bool obs_enum_output_protocols(size_t idx, char **protocol) + + Enumerates all registered protocol. + +--------------------- + +.. function:: void obs_enum_output_types_with_protocol(const char *protocol, void *data, bool (*enum_cb)(void *data, const char *id)) + + Enumerates through a callback all available output types for the given protocol. + + :param protocol: Protocol of the outputs to enumerate + :param data: Data passed to the callback + :param enum_cb: Callback used when a matching output is found, the id + of the output is passed to the callback + :return: When all outputs are enumerated or if the callback return *false* + +--------------------- + Functions used by outputs ------------------------- diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index 85a0c876235c0e..192022670c076a 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -137,7 +137,7 @@ Service Definition Structure (Optional) - :return: The output type that should be used with this service + :return: The output type that should be preferred with this service .. member:: const char **(*get_supported_video_codecs)(void *data) @@ -148,6 +148,49 @@ Service Definition Structure the data manually (typically best to use strlist_split to generate this) +.. member:: const char **(*get_supported_audio_codecs)(void *data) + + (Optional) + + :return: A string pointer array of the supported audio codecs, should + be stored by the plugin so the caller does not need to free + the data manually (typically best to use strlist_split to + generate this) + +.. member:: const char *(*obs_service_info.get_protocol)(void *data) + + :return: The protocol used by the service + +.. member:: const char *(*obs_service_info.get_connect_info)(void *data, uint32_t type) + + Output a service connection info related to a given type value: + + - **OBS_SERVICE_CONNECT_INFO_SERVER_URL** - Server URL (0) + + - **OBS_SERVICE_CONNECT_INFO_STREAM_ID** - Stream ID (2) + + - **OBS_SERVICE_CONNECT_INFO_STREAM_KEY** - Stream key (alias of **OBS_SERVICE_CONNECT_INFO_STREAM_ID**) + + - **OBS_SERVICE_CONNECT_INFO_USERNAME** - Username (4) + + - **OBS_SERVICE_CONNECT_INFO_PASSWORD** - Password (6) + + - **OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE** - Encryption passphrase (8) + + - Odd values as types are reserved for third-party protocols + + Depending on the protocol, the service will have to provide information + to the output to be able to connect. + + Irrelevant or unused types can return `NULL`. + +.. member:: bool (*obs_service_info.can_try_to_connect)(void *data) + + :return: If the service has all the needed connection info to be + able to connect. + + NOTE: If not set, :c:func:`obs_service_can_try_to_connect()` + returns *true* by default. General Service Functions ------------------------- @@ -270,24 +313,36 @@ General Service Functions :return: The URL currently used for this service +.. deprecated:: 29.1.0 + Use :c:func:`obs_service_get_connect_info()` instead. + --------------------- .. function:: const char *obs_service_get_key(const obs_service_t *service) :return: Stream key (if any) currently used for this service +.. deprecated:: 29.1.0 + Use :c:func:`obs_service_get_connect_info()` instead. + --------------------- .. function:: const char *obs_service_get_username(const obs_service_t *service) :return: User name (if any) currently used for this service +.. deprecated:: 29.1.0 + Use :c:func:`obs_service_get_connect_info()` instead. + --------------------- .. function:: const char *obs_service_get_password(const obs_service_t *service) :return: Password (if any) currently used for this service +.. deprecated:: 29.1.0 + Use :c:func:`obs_service_get_connect_info()` instead. + --------------------- .. function:: void obs_service_apply_encoder_settings(obs_service_t *service, obs_data_t *video_encoder_settings, obs_data_t *audio_encoder_settings) @@ -301,9 +356,41 @@ General Service Functions .. function:: const char **obs_service_get_supported_video_codecs(const obs_service_t *service) - :return: An array of string pointers containing the supported codecs - for the service, terminated with a *NULL* pointer. Does not - need to be freed + :return: An array of string pointers containing the supported video + codecs for the service, terminated with a *NULL* pointer. + Does not need to be freed + +--------------------- + +.. function:: const char **obs_service_get_supported_audio_codecs(const obs_service_t *service) + + :return: An array of string pointers containing the supported audio + codecs for the service, terminated with a *NULL* pointer. + Does not need to be freed + +--------------------- + +.. function:: const char *obs_service_get_protocol(const obs_service_t *service) + + :return: Protocol currently used for this service + +--------------------- + +.. function:: const char *obs_service_get_preferred_output_type(const obs_service_t *service) + + :return: The output type that should be preferred with this service + +.. function:: const char *obs_service_get_connect_info(const obs_service_t *service, uint32_t type) + + :param type: Check :c:member:`obs_service_info.get_connect_info` for + type values. + :return: Connection info related to the type value. + +.. function:: bool obs_service_can_try_to_connect(const obs_service_t *service) + + :return: If the service has all the needed connection info to be + able to connect. Returns `true` if + :c:member:`obs_service_info.can_try_to_connect` is not set. .. --------------------------------------------------------------------------- diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index f6a3835501998c..a67fb518cb8036 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -411,6 +411,8 @@ struct obs_core_data { obs_data_t *private_data; volatile bool valid; + + DARRAY(char *) protocols; }; /* user hotkeys */ diff --git a/libobs/obs-module.c b/libobs/obs-module.c index ff118814f75862..cd7841b6c1960d 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -836,6 +836,9 @@ void obs_register_output_s(const struct obs_output_info *info, size_t size) CHECK_REQUIRED_VAL_(info, start, obs_register_output); CHECK_REQUIRED_VAL_(info, stop, obs_register_output); + if (info->flags & OBS_OUTPUT_SERVICE) + CHECK_REQUIRED_VAL_(info, protocols, obs_register_output); + if (info->flags & OBS_OUTPUT_ENCODED) { CHECK_REQUIRED_VAL_(info, encoded_packet, obs_register_output); } else { @@ -856,6 +859,24 @@ void obs_register_output_s(const struct obs_output_info *info, size_t size) #undef CHECK_REQUIRED_VAL_ REGISTER_OBS_DEF(size, obs_output_info, obs->output_types, info); + + if (info->flags & OBS_OUTPUT_SERVICE) { + char **protocols = strlist_split(info->protocols, ';', false); + for (char **protocol = protocols; *protocol; ++protocol) { + bool skip = false; + for (size_t i = 0; i < obs->data.protocols.num; i++) { + if (strcmp(*protocol, + obs->data.protocols.array[i]) == 0) + skip = true; + } + + if (skip) + continue; + char *new_prtcl = bstrdup(*protocol); + da_push_back(obs->data.protocols, &new_prtcl); + } + strlist_free(protocols); + } return; error: @@ -907,6 +928,7 @@ void obs_register_service_s(const struct obs_service_info *info, size_t size) CHECK_REQUIRED_VAL_(info, get_name, obs_register_service); CHECK_REQUIRED_VAL_(info, create, obs_register_service); CHECK_REQUIRED_VAL_(info, destroy, obs_register_service); + CHECK_REQUIRED_VAL_(info, get_protocol, obs_register_service); #undef CHECK_REQUIRED_VAL_ REGISTER_OBS_DEF(size, obs_service_info, obs->service_types, info); diff --git a/libobs/obs-output.c b/libobs/obs-output.c index 335b7655541dd1..e46013a81227e5 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -290,7 +290,8 @@ bool obs_output_start(obs_output_t *output) return false; has_service = (output->info.flags & OBS_OUTPUT_SERVICE) != 0; - if (has_service && !obs_service_initialize(output->service, output)) + if (has_service && !(obs_service_can_try_to_connect(output->service) && + obs_service_initialize(output->service, output))) return false; encoded = (output->info.flags & OBS_OUTPUT_ENCODED) != 0; @@ -2713,3 +2714,53 @@ const char *obs_output_get_supported_audio_codecs(const obs_output_t *output) ? output->info.encoded_audio_codecs : NULL; } + +const char *obs_output_get_protocols(const obs_output_t *output) +{ + if (!obs_output_valid(output, "obs_output_get_protocols")) + return NULL; + + return (output->info.flags & OBS_OUTPUT_SERVICE) + ? output->info.protocols + : NULL; +} + +void obs_enum_output_types_with_protocol(const char *protocol, void *data, + bool (*enum_cb)(void *data, + const char *id)) +{ + if (!obs_is_output_protocol_registered(protocol)) + return; + + size_t protocol_len = strlen(protocol); + for (size_t i = 0; i < obs->output_types.num; i++) { + if (!(obs->output_types.array[i].flags & OBS_OUTPUT_SERVICE)) + continue; + + const char *substr = obs->output_types.array[i].protocols; + while (substr && substr[0] != '\0') { + const char *next = strchr(substr, ';'); + size_t len = next ? (size_t)(next - substr) + : strlen(substr); + if (protocol_len == len && + strncmp(substr, protocol, len) == 0) { + if (!enum_cb(data, + obs->output_types.array[i].id)) + return; + } + substr = next ? next + 1 : NULL; + } + } +} + +const char *obs_get_output_supported_video_codecs(const char *id) +{ + const struct obs_output_info *info = find_output(id); + return info ? info->encoded_video_codecs : NULL; +} + +const char *obs_get_output_supported_audio_codecs(const char *id) +{ + const struct obs_output_info *info = find_output(id); + return info ? info->encoded_audio_codecs : NULL; +} diff --git a/libobs/obs-output.h b/libobs/obs-output.h index e57659fd9f5104..0469d39a174733 100644 --- a/libobs/obs-output.h +++ b/libobs/obs-output.h @@ -77,6 +77,9 @@ struct obs_output_info { /* raw audio callback for multi track outputs */ void (*raw_audio2)(void *data, size_t idx, struct audio_data *frames); + + /* required if OBS_OUTPUT_SERVICE */ + const char *protocols; }; EXPORT void obs_register_output_s(const struct obs_output_info *info, diff --git a/libobs/obs-service.c b/libobs/obs-service.c index a2cd0140c6d1da..8ade7cb92cb02e 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -411,16 +411,6 @@ const char *obs_service_get_id(const obs_service_t *service) : NULL; } -const char *obs_service_get_output_type(const obs_service_t *service) -{ - if (!obs_service_valid(service, "obs_service_get_output_type")) - return NULL; - - if (service->info.get_output_type) - return service->info.get_output_type(service->context.data); - return NULL; -} - void obs_service_get_supported_resolutions( const obs_service_t *service, struct obs_service_resolution **resolutions, size_t *count) @@ -477,3 +467,58 @@ obs_service_get_supported_video_codecs(const obs_service_t *service) service->context.data); return NULL; } + +const char ** +obs_service_get_supported_audio_codecs(const obs_service_t *service) +{ + if (service->info.get_supported_audio_codecs) + return service->info.get_supported_audio_codecs( + service->context.data); + return NULL; +} + +const char *obs_service_get_protocol(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_get_protocol")) + return NULL; + + return service->info.get_protocol(service->context.data); +} + +/* OBS_DEPRECATED */ +const char *obs_service_get_output_type(const obs_service_t *service) +{ + return obs_service_get_preferred_output_type(service); +} + +const char *obs_service_get_preferred_output_type(const obs_service_t *service) +{ + if (!obs_service_valid(service, + "obs_service_get_preferred_output_type")) + return NULL; + + if (service->info.get_output_type) + return service->info.get_output_type(service->context.data); + return NULL; +} + +const char *obs_service_get_connect_info(const obs_service_t *service, + uint32_t type) +{ + if (!obs_service_valid(service, "obs_service_get_info")) + return NULL; + + if (!service->info.get_connect_info) + return NULL; + return service->info.get_connect_info(service->context.data, type); +} + +bool obs_service_can_try_to_connect(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_can_connect")) + return false; + + if (!service->info.can_try_to_connect) + return true; + return service->info.can_try_to_connect(service->context.data); +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index f025260ff1b70f..7f74fc1da0dd39 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -33,6 +33,17 @@ struct obs_service_resolution { int cy; }; +/* NOTE: Odd numbers are reserved for custom info from third-party protocols */ +enum obs_service_connect_info { + OBS_SERVICE_CONNECT_INFO_SERVER_URL = 0, + OBS_SERVICE_CONNECT_INFO_STREAM_ID = 2, + OBS_SERVICE_CONNECT_INFO_STREAM_KEY = + 2, // Alias of OBS_SERVICE_CONNECT_INFO_STREAM_ID + OBS_SERVICE_CONNECT_INFO_USERNAME = 4, + OBS_SERVICE_CONNECT_INFO_PASSWORD = 6, + OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE = 8, +}; + struct obs_service_info { /* required */ const char *id; @@ -77,6 +88,7 @@ struct obs_service_info { void *type_data; void (*free_type_data)(void *type_data); + /* TODO: Rename to 'get_preferred_output_type' once a API/ABI break happen */ const char *(*get_output_type)(void *data); void (*get_supported_resolutions)( @@ -88,6 +100,14 @@ struct obs_service_info { int *audio_bitrate); const char **(*get_supported_video_codecs)(void *data); + + const char *(*get_protocol)(void *data); + + const char **(*get_supported_audio_codecs)(void *data); + + const char *(*get_connect_info)(void *data, uint32_t type); + + bool (*can_try_to_connect)(void *data); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.c b/libobs/obs.c index cb17d5d8840832..73e281eaa83733 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -1078,6 +1078,10 @@ static void obs_free_data(void) da_free(data->draw_callbacks); da_free(data->tick_callbacks); obs_data_release(data->private_data); + + for (size_t i = 0; i < data->protocols.num; i++) + bfree(data->protocols.array[i]); + da_free(data->protocols); } static const char *obs_signals[] = { @@ -3452,3 +3456,22 @@ bool obs_weak_object_references_object(obs_weak_object_t *weak, { return weak && object && weak->object == object; } + +bool obs_is_output_protocol_registered(const char *protocol) +{ + for (size_t i = 0; i < obs->data.protocols.num; i++) { + if (strcmp(protocol, obs->data.protocols.array[i]) == 0) + return true; + } + + return false; +} + +bool obs_enum_output_protocols(size_t idx, char **protocol) +{ + if (idx >= obs->data.protocols.num) + return false; + + *protocol = obs->data.protocols.array[idx]; + return true; +} diff --git a/libobs/obs.h b/libobs/obs.h index f5b84b7082fba7..79d583042b5c7d 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2211,6 +2211,20 @@ obs_output_get_supported_video_codecs(const obs_output_t *output); EXPORT const char * obs_output_get_supported_audio_codecs(const obs_output_t *output); +EXPORT const char *obs_output_get_protocols(const obs_output_t *output); + +EXPORT bool obs_is_output_protocol_registered(const char *protocol); + +EXPORT bool obs_enum_output_protocols(size_t idx, char **protocol); + +EXPORT void obs_enum_output_types_with_protocol( + const char *protocol, void *data, + bool (*enum_cb)(void *data, const char *id)); + +EXPORT const char *obs_get_output_supported_video_codecs(const char *id); + +EXPORT const char *obs_get_output_supported_audio_codecs(const char *id); + /* ------------------------------------------------------------------------- */ /* Functions used by outputs */ @@ -2494,16 +2508,20 @@ EXPORT void obs_service_update(obs_service_t *service, obs_data_t *settings); EXPORT obs_data_t *obs_service_get_settings(const obs_service_t *service); /** Returns the URL for this service context */ -EXPORT const char *obs_service_get_url(const obs_service_t *service); +OBS_DEPRECATED EXPORT const char * +obs_service_get_url(const obs_service_t *service); /** Returns the stream key (if any) for this service context */ -EXPORT const char *obs_service_get_key(const obs_service_t *service); +OBS_DEPRECATED EXPORT const char * +obs_service_get_key(const obs_service_t *service); /** Returns the username (if any) for this service context */ -EXPORT const char *obs_service_get_username(const obs_service_t *service); +OBS_DEPRECATED EXPORT const char * +obs_service_get_username(const obs_service_t *service); /** Returns the password (if any) for this service context */ -EXPORT const char *obs_service_get_password(const obs_service_t *service); +OBS_DEPRECATED EXPORT const char * +obs_service_get_password(const obs_service_t *service); /** * Applies service-specific video encoder settings. @@ -2531,9 +2549,24 @@ EXPORT void obs_service_get_max_bitrate(const obs_service_t *service, EXPORT const char ** obs_service_get_supported_video_codecs(const obs_service_t *service); +EXPORT const char ** +obs_service_get_supported_audio_codecs(const obs_service_t *service); + /* NOTE: This function is temporary and should be removed/replaced at a later * date. */ -EXPORT const char *obs_service_get_output_type(const obs_service_t *service); +OBS_DEPRECATED EXPORT const char * +obs_service_get_output_type(const obs_service_t *service); + +/** Returns the protocol for this service context */ +EXPORT const char *obs_service_get_protocol(const obs_service_t *service); + +EXPORT const char * +obs_service_get_preferred_output_type(const obs_service_t *service); + +EXPORT const char *obs_service_get_connect_info(const obs_service_t *service, + uint32_t type); + +EXPORT bool obs_service_can_try_to_connect(const obs_service_t *service); /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ diff --git a/plugins/coreaudio-encoder/data/locale/en-US.ini b/plugins/coreaudio-encoder/data/locale/en-US.ini index 361a458ef27bb0..cf6be21cc54cdc 100644 --- a/plugins/coreaudio-encoder/data/locale/en-US.ini +++ b/plugins/coreaudio-encoder/data/locale/en-US.ini @@ -1,4 +1,4 @@ -CoreAudioAAC="CoreAudio AAC encoder" +CoreAudioAAC="CoreAudio AAC" Bitrate="Bitrate" AllowHEAAC="Allow HE-AAC" OutputSamplerate="Output Sample Rate" diff --git a/plugins/coreaudio-encoder/encoder.cpp b/plugins/coreaudio-encoder/encoder.cpp index 0cae2442edd192..999e67e61e9e77 100644 --- a/plugins/coreaudio-encoder/encoder.cpp +++ b/plugins/coreaudio-encoder/encoder.cpp @@ -1409,7 +1409,7 @@ bool obs_module_load(void) }; aac_info.id = "CoreAudio_AAC"; aac_info.type = OBS_ENCODER_AUDIO; - aac_info.codec = "AAC"; + aac_info.codec = "aac"; aac_info.get_name = aac_get_name; aac_info.destroy = aac_destroy; aac_info.create = aac_create; diff --git a/plugins/obs-ffmpeg/data/locale/en-US.ini b/plugins/obs-ffmpeg/data/locale/en-US.ini index 2f43ed0b5a66fa..6e7d1590335d9b 100644 --- a/plugins/obs-ffmpeg/data/locale/en-US.ini +++ b/plugins/obs-ffmpeg/data/locale/en-US.ini @@ -1,6 +1,6 @@ FFmpegOutput="FFmpeg Output" -FFmpegAAC="FFmpeg Default AAC Encoder" -FFmpegOpus="FFmpeg Opus Encoder" +FFmpegAAC="FFmpeg AAC" +FFmpegOpus="FFmpeg Opus" FFmpegOpts="FFmpeg Options" FFmpegOpts.ToolTip.Source="Allows you to set FFmpeg options. This only accepts options in the option=value format.\nMultiple options can be set by separating them with a space.\nExample: rtsp_transport=tcp rtsp_flags=prefer_tcp" Bitrate="Bitrate" diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c b/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c index 2562a63f1a886c..80920dcf95b958 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c @@ -427,7 +427,7 @@ static size_t enc_frame_size(void *data) struct obs_encoder_info aac_encoder_info = { .id = "ffmpeg_aac", .type = OBS_ENCODER_AUDIO, - .codec = "AAC", + .codec = "aac", .get_name = aac_getname, .create = aac_create, .destroy = enc_destroy, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-hls-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-hls-mux.c index 017ecba76aef5f..50da30a7ab5b6e 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-hls-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-hls-mux.c @@ -126,8 +126,10 @@ bool ffmpeg_hls_mux_start(void *data) service = obs_output_get_service(stream->output); if (!service) return false; - path_str = obs_service_get_url(service); - stream_key = obs_service_get_key(service); + path_str = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + stream_key = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_STREAM_KEY); dstr_copy(&stream->stream_key, stream_key); dstr_copy(&path, path_str); dstr_replace(&path, "{stream_key}", stream_key); @@ -331,6 +333,7 @@ struct obs_output_info ffmpeg_hls_muxer = { .id = "ffmpeg_hls_muxer", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | OBS_OUTPUT_SERVICE, + .protocols = "HLS", #ifdef ENABLE_HEVC .encoded_video_codecs = "h264;hevc", #else diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c b/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c index 5a20aae5132cfa..a59f04ccae0da9 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c @@ -349,24 +349,26 @@ static inline int connect_mpegts_url(struct ffmpeg_output *stream, bool is_rist) if (!is_rist) { SRTContext *context = (SRTContext *)uc->priv_data; context->streamid = NULL; - if (stream->ff_data.config.key != NULL) { - if (strlen(stream->ff_data.config.key)) - context->streamid = - av_strdup(stream->ff_data.config.key); + if (stream->ff_data.config.stream_id != NULL) { + if (strlen(stream->ff_data.config.stream_id)) + context->streamid = av_strdup( + stream->ff_data.config.stream_id); } context->passphrase = NULL; - if (stream->ff_data.config.password != NULL) { - if (strlen(stream->ff_data.config.password)) - context->passphrase = av_strdup( - stream->ff_data.config.password); + if (stream->ff_data.config.encrypt_passphrase != NULL) { + if (strlen(stream->ff_data.config.encrypt_passphrase)) + context->passphrase = + av_strdup(stream->ff_data.config + .encrypt_passphrase); } } else { RISTContext *context = (RISTContext *)uc->priv_data; context->secret = NULL; - if (stream->ff_data.config.key != NULL) { - if (strlen(stream->ff_data.config.key)) + if (stream->ff_data.config.encrypt_passphrase != NULL) { + if (strlen(stream->ff_data.config.encrypt_passphrase)) context->secret = - bstrdup(stream->ff_data.config.key); + bstrdup(stream->ff_data.config + .encrypt_passphrase); } context->username = NULL; if (stream->ff_data.config.username != NULL) { @@ -879,10 +881,16 @@ static bool set_config(struct ffmpeg_output *stream) service = obs_output_get_service(stream->output); if (!service) return false; - config.url = obs_service_get_url(service); - config.username = obs_service_get_username(service); - config.password = obs_service_get_password(service); - config.key = obs_service_get_key(service); + config.url = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + config.username = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_USERNAME); + config.password = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_PASSWORD); + config.stream_id = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_STREAM_ID); + config.encrypt_passphrase = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE); config.format_name = "mpegts"; config.format_mime_type = "video/M2PT"; @@ -955,11 +963,14 @@ static bool set_config(struct ffmpeg_output *stream) obs_data_release(settings); /* 3. Audio settings */ - // 3.a) set audio encoder and id to aac + // 3.a) set audio codec & id from audio encoder obs_encoder_t *aencoder = obs_output_get_audio_encoder(stream->output, 0); - config.audio_encoder = "aac"; - config.audio_encoder_id = AV_CODEC_ID_AAC; + config.audio_encoder = obs_encoder_get_codec(aencoder); + if (strcmp(config.audio_encoder, "aac") == 0) + config.audio_encoder_id = AV_CODEC_ID_AAC; + else if (strcmp(config.audio_encoder, "opus") == 0) + config.audio_encoder_id = AV_CODEC_ID_OPUS; // 3.b) get audio bitrate from the audio encoder. settings = obs_encoder_get_settings(aencoder); @@ -1289,12 +1300,13 @@ struct obs_output_info ffmpeg_mpegts_muxer = { .id = "ffmpeg_mpegts_muxer", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | OBS_OUTPUT_SERVICE, + .protocols = "SRT;RIST", #ifdef ENABLE_HEVC - .encoded_video_codecs = "h264;hevc;av1", + .encoded_video_codecs = "h264;hevc", #else - .encoded_video_codecs = "h264;av1", + .encoded_video_codecs = "h264", #endif - .encoded_audio_codecs = "aac", + .encoded_audio_codecs = "aac;opus", .get_name = ffmpeg_mpegts_getname, .create = ffmpeg_mpegts_create, .destroy = ffmpeg_mpegts_destroy, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index a605e9af352fc3..b821ada05d8081 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -419,7 +419,8 @@ static inline bool ffmpeg_mux_start_internal(struct ffmpeg_muxer *stream, service = obs_output_get_service(stream->output); if (!service) return false; - path = obs_service_get_url(service); + path = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); stream->split_file = false; } else { @@ -944,8 +945,9 @@ struct obs_output_info ffmpeg_mpegts_muxer = { .id = "ffmpeg_mpegts_muxer", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | OBS_OUTPUT_SERVICE, - .encoded_video_codecs = "h264;av1", - .encoded_audio_codecs = "aac", + .protocols = "SRT;RIST", + .encoded_video_codecs = "h264", + .encoded_audio_codecs = "aac;opus", .get_name = ffmpeg_mpegts_mux_getname, .create = ffmpeg_mux_create, .destroy = ffmpeg_mux_destroy, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.h b/plugins/obs-ffmpeg/obs-ffmpeg-output.h index 682eee30a90d3c..9bcf472f7233a5 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.h @@ -39,7 +39,8 @@ struct ffmpeg_cfg { int frame_size; // audio frame size const char *username; const char *password; - const char *key; + const char *stream_id; + const char *encrypt_passphrase; }; struct ffmpeg_audio_info { diff --git a/plugins/obs-libfdk/data/locale/en-US.ini b/plugins/obs-libfdk/data/locale/en-US.ini index cdf5f860369889..55420c526df1f4 100644 --- a/plugins/obs-libfdk/data/locale/en-US.ini +++ b/plugins/obs-libfdk/data/locale/en-US.ini @@ -1,3 +1,3 @@ -LibFDK="libfdk AAC Encoder" +LibFDK="libfdk AAC" Bitrate="Bitrate" Afterburner="Enable AAC Afterburner" diff --git a/plugins/obs-libfdk/obs-libfdk.c b/plugins/obs-libfdk/obs-libfdk.c index a02d8cf6bfda15..5e55489dc95c4d 100644 --- a/plugins/obs-libfdk/obs-libfdk.c +++ b/plugins/obs-libfdk/obs-libfdk.c @@ -305,7 +305,7 @@ static size_t libfdk_frame_size(void *data) struct obs_encoder_info obs_libfdk_encoder = { .id = "libfdk_aac", .type = OBS_ENCODER_AUDIO, - .codec = "AAC", + .codec = "aac", .get_name = libfdk_getname, .create = libfdk_create, .destroy = libfdk_destroy, diff --git a/plugins/obs-outputs/ftl-stream.c b/plugins/obs-outputs/ftl-stream.c index a0556d2a5d36d6..3d19b7563f7cf1 100644 --- a/plugins/obs-outputs/ftl-stream.c +++ b/plugins/obs-outputs/ftl-stream.c @@ -1049,7 +1049,8 @@ static int init_connect(struct ftl_stream *stream) obs_output_get_video_encoder(stream->output); obs_data_t *video_settings = obs_encoder_get_settings(video_encoder); - ingest_url = obs_service_get_url(service); + ingest_url = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); if (strncmp(ingest_url, FTL_URL_PROTOCOL, strlen(FTL_URL_PROTOCOL)) == 0) { dstr_copy(&stream->path, ingest_url + strlen(FTL_URL_PROTOCOL)); @@ -1057,7 +1058,8 @@ static int init_connect(struct ftl_stream *stream) dstr_copy(&stream->path, ingest_url); } - key = obs_service_get_key(service); + key = obs_service_get_connect_info(service, + OBS_SERVICE_CONNECT_INFO_STREAM_KEY); int target_bitrate = (int)obs_data_get_int(video_settings, "bitrate"); int peak_bitrate = (int)((float)target_bitrate * 1.1f); @@ -1093,8 +1095,12 @@ static int init_connect(struct ftl_stream *stream) } } - dstr_copy(&stream->username, obs_service_get_username(service)); - dstr_copy(&stream->password, obs_service_get_password(service)); + dstr_copy(&stream->username, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_USERNAME)); + dstr_copy(&stream->password, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_PASSWORD)); dstr_depad(&stream->path); stream->drop_threshold_usec = @@ -1159,6 +1165,7 @@ static int _ftl_error_to_obs_error(int status) struct obs_output_info ftl_output_info = { .id = "ftl_output", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE, + .protocols = "FTL", .encoded_video_codecs = "h264", .encoded_audio_codecs = "opus", .get_name = ftl_stream_getname, diff --git a/plugins/obs-outputs/rtmp-stream.c b/plugins/obs-outputs/rtmp-stream.c index f186c36161280e..0312101b711e37 100644 --- a/plugins/obs-outputs/rtmp-stream.c +++ b/plugins/obs-outputs/rtmp-stream.c @@ -1134,10 +1134,18 @@ static bool init_connect(struct rtmp_stream *stream) stream->got_first_video = false; settings = obs_output_get_settings(stream->output); - dstr_copy(&stream->path, obs_service_get_url(service)); - dstr_copy(&stream->key, obs_service_get_key(service)); - dstr_copy(&stream->username, obs_service_get_username(service)); - dstr_copy(&stream->password, obs_service_get_password(service)); + dstr_copy(&stream->path, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL)); + dstr_copy(&stream->key, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_STREAM_KEY)); + dstr_copy(&stream->username, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_USERNAME)); + dstr_copy(&stream->password, + obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_PASSWORD)); dstr_depad(&stream->path); dstr_depad(&stream->key); drop_b = (int64_t)obs_data_get_int(settings, OPT_DROP_THRESHOLD); @@ -1642,6 +1650,11 @@ struct obs_output_info rtmp_output_info = { .id = "rtmp_output", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE | OBS_OUTPUT_MULTI_TRACK, +#ifdef NO_CRYPTO + .protocols = "RTMP", +#else + .protocols = "RTMP;RTMPS", +#endif .encoded_video_codecs = "h264", .encoded_audio_codecs = "aac", .get_name = rtmp_stream_getname, diff --git a/plugins/rtmp-services/data/package.json b/plugins/rtmp-services/data/package.json index 89e6d4f31d1c14..45d0c1fe310681 100644 --- a/plugins/rtmp-services/data/package.json +++ b/plugins/rtmp-services/data/package.json @@ -1,11 +1,11 @@ { "$schema": "schema/package-schema.json", "url": "https://obsproject.com/obs2_update/rtmp-services/v4", - "version": 220, + "version": 222, "files": [ { "name": "services.json", - "version": 220 + "version": 222 } ] } diff --git a/plugins/rtmp-services/data/schema/service-schema-v4.json b/plugins/rtmp-services/data/schema/service-schema-v4.json index 61f3c0cf2d37e9..88b33abd3a93b8 100644 --- a/plugins/rtmp-services/data/schema/service-schema-v4.json +++ b/plugins/rtmp-services/data/schema/service-schema-v4.json @@ -16,6 +16,18 @@ "description": "Name of the streaming service. Will be displayed in the Service dropdown.", "minLength": 1 }, + "protocol" : { + "type": "string", + "description": "Protocol used by the service. If missing the service is considered using RTMP or RTMPS.", + "enum": [ + "RTMP", + "RTMPS", + "HLS", + "FTL", + "SRT", + "RIST" + ] + }, "common": { "type": "boolean", "description": "Whether or not the service is shown in the list before it is expanded to all services by the user.", @@ -106,7 +118,7 @@ }, "output": { "type": "string", - "description": "OBS output module used.", + "description": "OBS output module used. Unused since OBS Studio 30, kept for backward compatibility.", "enum": [ "rtmp_output", "ffmpeg_hls_muxer", @@ -200,6 +212,18 @@ "required": [ "name", "servers" + ], + "allOf": [ + { + "$comment": "Require protocol field if not an RTMP(S) URL", + "if": { "not": { "properties": { "servers": { "items": { "properties": { "url": { "format": "uri", "pattern": "^rtmps?://" } } } } } } }, + "then": { "required": ["protocol"] } + }, + { + "$comment": "Require recommended output field if protocol field is not RTMP(S)", + "if": { "required": ["protocol"], "properties": { "protocol": { "pattern": "^(HLS|SRT|RIST|FTL)$" } } }, + "then": { "properties": { "recommended": { "required": ["output"] } } } + } ] }, "additionalItems": true diff --git a/plugins/rtmp-services/data/services.json b/plugins/rtmp-services/data/services.json index c9f8391ddd722e..7615ede461fc91 100644 --- a/plugins/rtmp-services/data/services.json +++ b/plugins/rtmp-services/data/services.json @@ -204,6 +204,7 @@ "common": false, "more_info_link": "https://developers.google.com/youtube/v3/live/guides/ingestion-protocol-comparison", "stream_key_link": "https://www.youtube.com/live_dashboard", + "protocol": "HLS", "supported video codecs": [ "h264", "hevc" @@ -1445,9 +1446,7 @@ { "name": "YouNow", "common": false, - "supported audio codecs": [ - "opus" - ], + "protocol": "FTL", "servers": [ { "name": "younow.com", @@ -1687,6 +1686,7 @@ }, { "name": "SHOWROOM", + "protocol": "RTMP", "servers": [ { "name": "Default", @@ -1821,9 +1821,7 @@ { "name": "Glimesh", "stream_key_link": "https://glimesh.tv/users/settings/stream", - "supported audio codecs": [ - "opus" - ], + "protocol": "FTL", "servers": [ { "name": "North America - Chicago, United States", @@ -2024,6 +2022,7 @@ }, { "name": "Dacast", + "protocol": "RTMP", "servers": [ { "name": "Default", diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 1aa20ec6758772..77b4b74adc34f8 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -13,15 +13,16 @@ struct rtmp_common { char *service; + char *protocol; char *server; char *key; - char *output; struct obs_service_resolution *supported_resolutions; size_t supported_resolutions_count; int max_fps; char **video_codecs; + char **audio_codecs; bool supports_additional_audio_track; }; @@ -76,10 +77,6 @@ static void ensure_valid_url(struct rtmp_common *service, json_t *json, static void update_recommendations(struct rtmp_common *service, json_t *rec) { - const char *out = get_string_val(rec, "output"); - if (out) - service->output = bstrdup(out); - json_t *sr = json_object_get(rec, "supported resolutions"); if (sr && json_is_array(sr)) { DARRAY(struct obs_service_resolution) res_list; @@ -116,18 +113,22 @@ static void rtmp_common_update(void *data, obs_data_t *settings) struct rtmp_common *service = data; bfree(service->supported_resolutions); - bfree(service->video_codecs); + if (service->video_codecs) + bfree(service->video_codecs); + if (service->audio_codecs) + bfree(service->audio_codecs); bfree(service->service); + bfree(service->protocol); bfree(service->server); - bfree(service->output); bfree(service->key); service->service = bstrdup(obs_data_get_string(settings, "service")); + service->protocol = bstrdup(obs_data_get_string(settings, "protocol")); service->server = bstrdup(obs_data_get_string(settings, "server")); service->key = bstrdup(obs_data_get_string(settings, "key")); service->supports_additional_audio_track = false; - service->output = NULL; service->video_codecs = NULL; + service->audio_codecs = NULL; service->supported_resolutions = NULL; service->supported_resolutions_count = 0; service->max_fps = 0; @@ -154,9 +155,6 @@ static void rtmp_common_update(void *data, obs_data_t *settings) } } json_decref(root); - - if (!service->output) - service->output = bstrdup("rtmp_output"); } static void rtmp_common_destroy(void *data) @@ -164,10 +162,13 @@ static void rtmp_common_destroy(void *data) struct rtmp_common *service = data; bfree(service->supported_resolutions); - bfree(service->video_codecs); + if (service->video_codecs) + bfree(service->video_codecs); + if (service->audio_codecs) + bfree(service->audio_codecs); bfree(service->service); + bfree(service->protocol); bfree(service->server); - bfree(service->output); bfree(service->key); bfree(service); } @@ -208,6 +209,35 @@ static inline bool get_bool_val(json_t *service, const char *key) return json_is_true(bool_val); } +#define RTMP_PREFIX "rtmp://" +#define RTMPS_PREFIX "rtmps://" + +static bool is_protocol_available(json_t *service) +{ + const char *protocol = get_string_val(service, "protocol"); + if (protocol) + return obs_is_output_protocol_registered(protocol); + + /* Test RTMP and RTMPS if no protocol found */ + json_t *servers; + size_t index; + json_t *server; + const char *url; + bool ret = false; + + servers = json_object_get(service, "servers"); + json_array_foreach (servers, index, server) { + url = get_string_val(server, "url"); + + if (strncmp(url, RTMP_PREFIX, strlen(RTMP_PREFIX)) == 0) + ret |= obs_is_output_protocol_registered("RTMP"); + else if (strncmp(url, RTMPS_PREFIX, strlen(RTMPS_PREFIX)) == 0) + ret |= obs_is_output_protocol_registered("RTMPS"); + } + + return ret; +} + static void add_service(obs_property_t *list, json_t *service, bool show_all, const char *cur_service) { @@ -258,6 +288,9 @@ static void add_services(obs_property_t *list, json_t *root, bool show_all, } json_array_foreach (root, index, service) { + /* Skip service with non-available protocol */ + if (!is_protocol_available(service)) + continue; add_service(list, service, show_all, cur_service); } @@ -399,11 +432,13 @@ static void fill_servers(obs_property_t *servers_prop, json_t *service, return; } + /* Assumption: Twitch should be RTMP only, so no RTMPS check */ if (strcmp(name, "Twitch") == 0) { if (fill_twitch_servers(servers_prop)) return; } + /* Assumption: Nimo TV should be RTMP only, so no RTMPS check in the ingest */ if (strcmp(name, "Nimo TV") == 0) { obs_property_list_add_string( servers_prop, obs_module_text("Server.Auto"), "auto"); @@ -416,6 +451,11 @@ static void fill_servers(obs_property_t *servers_prop, json_t *service, if (!server_name || !url) continue; + /* Skip RTMPS server if protocol not registered */ + if ((!obs_is_output_protocol_registered("RTMPS")) && + (strncmp(url, "rtmps://", 8) == 0)) + continue; + obs_property_list_add_string(servers_prop, server_name, url); } } @@ -439,6 +479,29 @@ static void fill_stream_key_link(json_t *service, obs_data_t *settings) stream_key_link); } +static void update_protocol(json_t *service, obs_data_t *settings) +{ + const char *protocol = get_string_val(service, "protocol"); + if (protocol) { + obs_data_set_string(settings, "protocol", protocol); + return; + } + + json_t *servers = json_object_get(service, "servers"); + if (!json_is_array(servers)) + return; + + json_t *server = json_array_get(servers, 0); + const char *url = get_string_val(server, "url"); + + if (strncmp(url, RTMPS_PREFIX, strlen(RTMPS_PREFIX)) == 0) { + obs_data_set_string(settings, "protocol", "RTMPS"); + return; + } + + obs_data_set_string(settings, "protocol", "RTMP"); +} + static inline json_t *find_service(json_t *root, const char *name, const char **p_new_name) { @@ -449,6 +512,10 @@ static inline json_t *find_service(json_t *root, const char *name, *p_new_name = NULL; json_array_foreach (root, index, service) { + /* skip service with non-available protocol */ + if (!is_protocol_available(service)) + continue; + const char *cur_name = get_string_val(service, "name"); if (strcmp(name, cur_name) == 0) @@ -503,6 +570,7 @@ static bool service_selected(obs_properties_t *props, obs_property_t *p, fill_servers(obs_properties_get(props, "server"), service, name); fill_more_info_link(service, settings); fill_stream_key_link(service, settings); + update_protocol(service, settings); return true; } @@ -667,12 +735,6 @@ static void rtmp_common_apply_settings(void *data, obs_data_t *video_settings, } } -static const char *rtmp_common_get_output_type(void *data) -{ - struct rtmp_common *service = data; - return service->output; -} - static const char *rtmp_common_url(void *data) { struct rtmp_common *service = data; @@ -874,21 +936,20 @@ static const char **rtmp_common_get_supported_video_codecs(void *data) json_t *json_video_codecs = json_object_get(json_service, "supported video codecs"); - if (json_is_array(json_video_codecs)) { - size_t index; - json_t *item; + if (!json_is_array(json_video_codecs)) { + goto fail; + } - json_array_foreach (json_video_codecs, index, item) { - char codec[16]; + size_t index; + json_t *item; - snprintf(codec, sizeof(codec), "%s", - json_string_value(item)); - if (codecs.len) - dstr_cat(&codecs, ";"); - dstr_cat(&codecs, codec); - } - } else { - dstr_cat(&codecs, "h264;"); + json_array_foreach (json_video_codecs, index, item) { + char codec[16]; + + snprintf(codec, sizeof(codec), "%s", json_string_value(item)); + if (codecs.len) + dstr_cat(&codecs, ";"); + dstr_cat(&codecs, codec); } service->video_codecs = strlist_split(codecs.array, ';', false); @@ -899,6 +960,49 @@ static const char **rtmp_common_get_supported_video_codecs(void *data) return (const char **)service->video_codecs; } +static const char **rtmp_common_get_supported_audio_codecs(void *data) +{ + struct rtmp_common *service = data; + + if (service->audio_codecs) + return (const char **)service->audio_codecs; + + struct dstr codecs = {0}; + json_t *root = open_services_file(); + if (!root) + return NULL; + + json_t *json_service = find_service(root, service->service, NULL); + if (!json_service) { + goto fail; + } + + json_t *json_audio_codecs = + json_object_get(json_service, "supported audio codecs"); + if (json_is_array(json_audio_codecs)) { + goto fail; + } + + size_t index; + json_t *item; + + json_array_foreach (json_audio_codecs, index, item) { + char codec[16]; + + snprintf(codec, sizeof(codec), "%s", json_string_value(item)); + if (codecs.len) + dstr_cat(&codecs, ";"); + dstr_cat(&codecs, codec); + } + + service->audio_codecs = strlist_split(codecs.array, ';', false); + dstr_free(&codecs); + +fail: + json_decref(root); + return (const char **)service->audio_codecs; +} + static const char *rtmp_common_username(void *data) { struct rtmp_common *service = data; @@ -925,6 +1029,53 @@ static const char *rtmp_common_password(void *data) return NULL; } +static const char *rtmp_common_get_protocol(void *data) +{ + struct rtmp_common *service = data; + + return service->protocol ? service->protocol : "RTMP"; +} + +static const char *rtmp_common_get_connect_info(void *data, uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return rtmp_common_url(data); + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + return rtmp_common_key(data); + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return rtmp_common_username(data); + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return rtmp_common_password(data); + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: { + const char *protocol = rtmp_common_get_protocol(data); + + if ((strcmp(protocol, "SRT") == 0)) + return rtmp_common_password(data); + else if ((strcmp(protocol, "RIST") == 0)) + return rtmp_common_key(data); + + break; + } + } + + return NULL; +} + +static bool rtmp_common_can_try_to_connect(void *data) +{ + struct rtmp_common *service = data; + const char *key = rtmp_common_key(data); + + if (service->service && strcmp(service->service, "Dacast") == 0) + return (key != NULL && key[0] != '\0'); + + const char *url = rtmp_common_url(data); + + return (url != NULL && url[0] != '\0') && + (key != NULL && key[0] != '\0'); +} + struct obs_service_info rtmp_common_service = { .id = "rtmp_common", .get_name = rtmp_common_getname, @@ -932,14 +1083,17 @@ struct obs_service_info rtmp_common_service = { .destroy = rtmp_common_destroy, .update = rtmp_common_update, .get_properties = rtmp_common_properties, + .get_protocol = rtmp_common_get_protocol, .get_url = rtmp_common_url, .get_key = rtmp_common_key, .get_username = rtmp_common_username, .get_password = rtmp_common_password, + .get_connect_info = rtmp_common_get_connect_info, .apply_encoder_settings = rtmp_common_apply_settings, - .get_output_type = rtmp_common_get_output_type, .get_supported_resolutions = rtmp_common_get_supported_resolutions, .get_max_fps = rtmp_common_get_max_fps, .get_max_bitrate = rtmp_common_get_max_bitrate, .get_supported_video_codecs = rtmp_common_get_supported_video_codecs, + .get_supported_audio_codecs = rtmp_common_get_supported_audio_codecs, + .can_try_to_connect = rtmp_common_can_try_to_connect, }; diff --git a/plugins/rtmp-services/rtmp-custom.c b/plugins/rtmp-services/rtmp-custom.c index 51276d6c8a45d0..a4f6cd810ce2bd 100644 --- a/plugins/rtmp-services/rtmp-custom.c +++ b/plugins/rtmp-services/rtmp-custom.c @@ -110,6 +110,30 @@ static const char *rtmp_custom_password(void *data) return service->password; } +#define RTMPS_PREFIX "rtmps://" +#define FTL_PREFIX "ftl://" +#define SRT_PREFIX "srt://" +#define RIST_PREFIX "rist://" + +static const char *rtmp_custom_get_protocol(void *data) +{ + struct rtmp_custom *service = data; + + if (strncmp(service->server, RTMPS_PREFIX, strlen(RTMPS_PREFIX)) == 0) + return "RTMPS"; + + if (strncmp(service->server, FTL_PREFIX, strlen(FTL_PREFIX)) == 0) + return "FTL"; + + if (strncmp(service->server, SRT_PREFIX, strlen(SRT_PREFIX)) == 0) + return "SRT"; + + if (strncmp(service->server, RIST_PREFIX, strlen(RIST_PREFIX)) == 0) + return "RIST"; + + return "RTMP"; +} + #define RTMP_PROTOCOL "rtmp" static void rtmp_custom_apply_settings(void *data, obs_data_t *video_settings, @@ -124,6 +148,39 @@ static void rtmp_custom_apply_settings(void *data, obs_data_t *video_settings, } } +static const char *rtmp_custom_get_connect_info(void *data, uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return rtmp_custom_url(data); + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + return rtmp_custom_key(data); + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return rtmp_custom_username(data); + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return rtmp_custom_password(data); + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: { + const char *protocol = rtmp_custom_get_protocol(data); + + if ((strcmp(protocol, "SRT") == 0)) + return rtmp_custom_password(data); + else if ((strcmp(protocol, "RIST") == 0)) + return rtmp_custom_key(data); + + break; + } + } + + return NULL; +} + +static bool rtmp_custom_can_try_to_connect(void *data) +{ + struct rtmp_custom *service = data; + + return (service->server != NULL && service->server[0] != '\0'); +} + struct obs_service_info rtmp_custom_service = { .id = "rtmp_custom", .get_name = rtmp_custom_name, @@ -131,9 +188,12 @@ struct obs_service_info rtmp_custom_service = { .destroy = rtmp_custom_destroy, .update = rtmp_custom_update, .get_properties = rtmp_custom_properties, + .get_protocol = rtmp_custom_get_protocol, .get_url = rtmp_custom_url, .get_key = rtmp_custom_key, + .get_connect_info = rtmp_custom_get_connect_info, .get_username = rtmp_custom_username, .get_password = rtmp_custom_password, .apply_encoder_settings = rtmp_custom_apply_settings, + .can_try_to_connect = rtmp_custom_can_try_to_connect, };