diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 5586a7301..da98079a0 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -394,6 +394,8 @@ Output.BadPath.Text="The configured file output path is invalid. Please check yo
# broadcast setup messages
Output.NoBroadcast.Title="No Broadcast Configured"
Output.NoBroadcast.Text="You need to set up a broadcast before you can start streaming."
+Output.BroadcastStartFailed="Failed to start broadcast"
+Output.BroadcastStopFailed="Failed to stop broadcast"
# log upload dialog text and messages
LogReturnDialog="Log Upload Successful"
@@ -1248,6 +1250,8 @@ YouTube.Actions.Error.BroadcastNotFound="The selected broadcast was not found."
YouTube.Actions.Error.FileMissing="Selected file does not exist."
YouTube.Actions.Error.FileOpeningFailed="Failed opening selected file."
YouTube.Actions.Error.FileTooLarge="Selected file is too large (Limit: 2 MiB)."
+YouTube.Actions.Error.BroadcastTransitionFailed="Transitioning the broadcast failed: %1
If this error persists open the broadcast in YouTube Studio and try manually."
+YouTube.Actions.Error.BroadcastTestStarting="Broadcast is transitioning to the test stage, this can take some time. Please try again in 10-30 seconds."
YouTube.Actions.EventsLoading="Loading list of events..."
YouTube.Actions.EventCreated.Title="Event Created"
@@ -1268,3 +1272,6 @@ YouTube.Actions.AutoStopStreamingWarning="You will not be able to reconnect.
# YouTube API errors in format "YouTube.Errors."
YouTube.Errors.liveStreamingNotEnabled="Live streaming is not enabled on the selected YouTube channel.
See youtube.com/features for more information."
YouTube.Errors.livePermissionBlocked="Live streaming is unavailable on the selected YouTube Channel.
Please note that it may take up to 24 hours for live streaming to become available after enabling it in your channel settings.
See youtube.com/features for details."
+YouTube.Errors.errorExecutingTransition="Transition failed due to a backend error. Please try again in a few seconds."
+YouTube.Errors.errorStreamInactive="YouTube is not receiving data for your stream. Please check your configuration and try again."
+YouTube.Errors.invalidTransition="The attempted transition was invalid. This may be due to the stream not having finished a previous transition. Please wait a few seconds and try again."
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index 677421b2a..3ab7d3518 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -6296,14 +6296,19 @@ void OBSBasic::StartStreaming()
ui->broadcastButton->style()->polish(ui->broadcastButton);
// well, we need to disable button while stream is not active
ui->broadcastButton->setEnabled(false);
- } else if (!autoStopBroadcast) {
- broadcastActive = true;
- ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast"));
+ } else {
+ if (!autoStopBroadcast) {
+ ui->broadcastButton->setText(
+ QTStr("Basic.Main.StopBroadcast"));
+ } else {
+ ui->broadcastButton->setText(
+ QTStr("Basic.Main.AutoStopEnabled"));
+ ui->broadcastButton->setEnabled(false);
+ }
ui->broadcastButton->setProperty("broadcastState", "active");
ui->broadcastButton->style()->unpolish(ui->broadcastButton);
ui->broadcastButton->style()->polish(ui->broadcastButton);
- } else {
- ui->broadcastButton->setEnabled(false);
+ broadcastActive = true;
}
bool recordWhenStreaming = config_get_bool(
@@ -6337,7 +6342,24 @@ void OBSBasic::BroadcastButtonClicked()
std::shared_ptr ytAuth =
dynamic_pointer_cast(auth);
if (ytAuth.get()) {
- ytAuth->StartLatestBroadcast();
+ if (!ytAuth->StartLatestBroadcast()) {
+ auto last_error = ytAuth->GetLastError();
+ if (last_error.isEmpty())
+ last_error = QTStr(
+ "YouTube.Actions.Error.YouTubeApi");
+ if (!ytAuth->GetTranslatedError(last_error))
+ last_error =
+ QTStr("YouTube.Actions.Error.BroadcastTransitionFailed")
+ .arg(last_error,
+ ytAuth->GetBroadcastId());
+
+ OBSMessageBox::warning(
+ this,
+ QTStr("Output.BroadcastStartFailed"),
+ last_error, true);
+ ui->broadcastButton->setChecked(false);
+ return;
+ }
}
#endif
broadcastActive = true;
@@ -6375,7 +6397,22 @@ void OBSBasic::BroadcastButtonClicked()
std::shared_ptr ytAuth =
dynamic_pointer_cast(auth);
if (ytAuth.get()) {
- ytAuth->StopLatestBroadcast();
+ if (!ytAuth->StopLatestBroadcast()) {
+ auto last_error = ytAuth->GetLastError();
+ if (last_error.isEmpty())
+ last_error = QTStr(
+ "YouTube.Actions.Error.YouTubeApi");
+ if (!ytAuth->GetTranslatedError(last_error))
+ last_error =
+ QTStr("YouTube.Actions.Error.BroadcastTransitionFailed")
+ .arg(last_error,
+ ytAuth->GetBroadcastId());
+
+ OBSMessageBox::warning(
+ this,
+ QTStr("Output.BroadcastStopFailed"),
+ last_error, true);
+ }
}
#endif
broadcastActive = false;
diff --git a/UI/youtube-api-wrappers.cpp b/UI/youtube-api-wrappers.cpp
index 80d53a377..e686fc2e3 100644
--- a/UI/youtube-api-wrappers.cpp
+++ b/UI/youtube-api-wrappers.cpp
@@ -405,7 +405,31 @@ bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
lastErrorMessage.clear();
lastErrorReason.clear();
- if (!ResetBroadcast(broadcast_id))
+ Json json_out;
+ if (!FindBroadcast(broadcast_id, json_out))
+ return false;
+
+ auto lifeCycleStatus =
+ json_out["items"][0]["status"]["lifeCycleStatus"].string_value();
+
+ if (lifeCycleStatus == "live" || lifeCycleStatus == "liveStarting")
+ // Broadcast is already (going to be) live
+ return true;
+ else if (lifeCycleStatus == "testStarting") {
+ // User will need to wait a few seconds before attempting to start broadcast
+ lastErrorMessage =
+ QTStr("YouTube.Actions.Error.BroadcastTestStarting");
+ lastErrorReason.clear();
+ return false;
+ }
+
+ // Only reset if broadcast has monitoring enabled and is not already in "testing" mode
+ auto monitorStreamEnabled =
+ json_out["items"][0]["contentDetails"]["monitorStream"]
+ ["enableMonitorStream"]
+ .bool_value();
+ if (lifeCycleStatus != "testing" && monitorStreamEnabled &&
+ !ResetBroadcast(broadcast_id, json_out))
return false;
const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
@@ -413,9 +437,10 @@ bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
"&broadcastStatus=%2"
"&part=status";
const QString live = url_template.arg(broadcast_id, "live");
- Json json_out;
- return InsertCommand(QT_TO_UTF8(live), "application/json", "POST", "{}",
- json_out);
+ bool success = InsertCommand(QT_TO_UTF8(live), "application/json",
+ "POST", "{}", json_out);
+ // Return a success if the command failed, but was redundant (broadcast already live)
+ return success || lastErrorReason == "redundantTransition";
}
bool YoutubeApiWrappers::StartLatestBroadcast()
@@ -434,8 +459,10 @@ bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id)
"&part=status";
const QString url = url_template.arg(broadcast_id);
Json json_out;
- return InsertCommand(QT_TO_UTF8(url), "application/json", "POST", "{}",
- json_out);
+ bool success = InsertCommand(QT_TO_UTF8(url), "application/json",
+ "POST", "{}", json_out);
+ // Return a success if the command failed, but was redundant (broadcast already stopped)
+ return success || lastErrorReason == "redundantTransition";
}
bool YoutubeApiWrappers::StopLatestBroadcast()
@@ -453,24 +480,12 @@ QString YoutubeApiWrappers::GetBroadcastId()
return this->broadcast_id;
}
-bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id)
+bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id,
+ json11::Json &json_out)
{
lastErrorMessage.clear();
lastErrorReason.clear();
- const QString url_template = YOUTUBE_LIVE_BROADCAST_URL
- "?part=id,snippet,contentDetails,status"
- "&id=%1";
- const QString url = url_template.arg(broadcast_id);
- Json json_out;
-
- if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
- json_out))
- return false;
-
- const QString put = YOUTUBE_LIVE_BROADCAST_URL
- "?part=id,snippet,contentDetails,status";
-
auto snippet = json_out["items"][0]["snippet"];
auto status = json_out["items"][0]["status"];
auto contentDetails = json_out["items"][0]["contentDetails"];
@@ -514,6 +529,9 @@ bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id)
{"startWithSlate", contentDetails["startWithSlate"]},
}},
};
+
+ const QString put = YOUTUBE_LIVE_BROADCAST_URL
+ "?part=id,snippet,contentDetails,status";
return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT",
data.dump().c_str(), json_out);
}
diff --git a/UI/youtube-api-wrappers.hpp b/UI/youtube-api-wrappers.hpp
index 1a6bc4f26..6fc939ee2 100644
--- a/UI/youtube-api-wrappers.hpp
+++ b/UI/youtube-api-wrappers.hpp
@@ -70,7 +70,8 @@ public:
const QString &thumbnail_file);
bool StartBroadcast(const QString &broadcast_id);
bool StopBroadcast(const QString &broadcast_id);
- bool ResetBroadcast(const QString &broadcast_id);
+ bool ResetBroadcast(const QString &broadcast_id,
+ json11::Json &json_out);
bool StartLatestBroadcast();
bool StopLatestBroadcast();