Skip to content

Release/v1.3.0 - #19

Open
willyliu-17 wants to merge 1 commit into
mainfrom
release/v1.3.0
Open

Release/v1.3.0#19
willyliu-17 wants to merge 1 commit into
mainfrom
release/v1.3.0

Conversation

@willyliu-17

Copy link
Copy Markdown
Collaborator

Stability & Crash Fixes:

  • Fixed multiple crashes occurring during shutdown, logout, authorization, and stream stopping.
  • Resolved memory safety issues and hanging processes when exiting.

Chat System:

  • Refactored chat to use native OBS Browser Dock and integrated Ably.
  • Improved connectivity, reconnection logic, and history loading for Twitch and YouTube.

Multi-RTMP:

  • Enhanced error reporting and added re-authorization prompts.
  • Fixed "Stop All" functionality and optimized video/audio encoder settings.

UI & Features:

  • Added support for gift animations in the preview window.
  • Renamed "Rock Zone" to "Rock Area" and fixed list display and deletion bugs.
  • Expanded crash report collection capabilities in diagnostics.

@willyliu-17
willyliu-17 requested a review from YC-Chang July 31, 2026 10:37

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new features and refactorings to the OBS 17LIVE plugin, including a custom animation trigger service, an automatic crash log upload service, YouTube broadcast and chat integration, user profile notes, and a modularized core runtime. It also updates installers to support Steam OBS. The code review identified several critical issues that need to be addressed: thread-safety and performance bottlenecks (such as holding a global mutex during network requests and synchronously probing video duration on the UI thread), potential race conditions and crashes during thread and CEF widget destruction, a discrepancy in the Windows installer path, and a logic bug in the default badge fallback.

Comment on lines +247 to +261
std::lock_guard<std::mutex> lock(asset_cache_mutex_);

const QFileInfo cache_info(file_path);
if (cache_info.exists() && cache_info.isFile() && cache_info.size() > 0) {
return true;
}

std::string content;
if (!download_remote_asset_content(source_url, content, error_message)) {
return false;
}

if (!persist_cached_remote_asset(cached_file_path, content, error_message)) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Holding a global mutex lock (asset_cache_mutex_) during a synchronous network request (download_remote_asset_content) blocks all other threads trying to access or cache other assets. This can cause severe performance bottlenecks and UI unresponsiveness. Consider performing the download outside of the critical section, and only lock when checking or writing to the cache file.

    {
        std::lock_guard<std::mutex> lock(asset_cache_mutex_);
        const QFileInfo cache_info(file_path);
        if (cache_info.exists() && cache_info.isFile() && cache_info.size() > 0) {
            return true;
        }
    }

    std::string content;
    if (!download_remote_asset_content(source_url, content, error_message)) {
        return false;
    }

    std::lock_guard<std::mutex> lock(asset_cache_mutex_);
    if (!persist_cached_remote_asset(cached_file_path, content, error_message)) {
        return false;
    }

Comment on lines +570 to 583
if (timer) {
if (timer->thread() == QThread::currentThread()) {
timer->stop();
timer->deleteLater();
} else {
QMetaObject::invokeMethod(
timer,
[timer]() {
timer->stop();
timer->deleteLater();
},
Qt::QueuedConnection);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Qt::QueuedConnection to stop and delete the timer asynchronously from a background thread creates a race condition. The controller object (this) can be fully destroyed before the timer is stopped, leading to a use-after-free crash when the timer fires in the interim. Use Qt::BlockingQueuedConnection instead to ensure the timer is stopped synchronously before the calling thread proceeds with destruction.

Suggested change
if (timer) {
if (timer->thread() == QThread::currentThread()) {
timer->stop();
timer->deleteLater();
} else {
QMetaObject::invokeMethod(
timer,
[timer]() {
timer->stop();
timer->deleteLater();
},
Qt::QueuedConnection);
}
}
if (timer) {
if (timer->thread() == QThread::currentThread()) {
timer->stop();
timer->deleteLater();
} else {
QMetaObject::invokeMethod(
timer,
[timer]() {
timer->stop();
timer->deleteLater();
},
Qt::BlockingQueuedConnection);
}
}

Comment on lines +440 to +480
if (type == "video" && obs_source_get_display_name("ffmpeg_source")) {
const std::string pathStd = destPath.toStdString();
const std::string probeName =
"17LiveCartoonDurationProbe_" + std::to_string(QDateTime::currentMSecsSinceEpoch());

auto prom = std::make_shared<std::promise<int64_t>>();
auto fut = prom->get_future();

ScheduleOBSTask([pathStd, probeName, prom]() mutable {
int64_t durationMs = 0;
ObsDataPtr settings(obs_data_create());
obs_data_set_bool(settings.get(), "looping", false);
obs_data_set_bool(settings.get(), "restart_on_activate", true);
obs_data_set_bool(settings.get(), "close_when_inactive", true);
obs_data_set_string(settings.get(), "local_file", pathStd.c_str());

obs_source_t* probe =
obs_source_create("ffmpeg_source", probeName.c_str(), settings.get(), nullptr);
if (probe) {
obs_source_media_restart(probe);
for (int i = 0; i < 40; i++) {
durationMs = obs_source_media_get_duration(probe);
if (durationMs > 0) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
obs_source_release(probe);
}
prom->set_value(durationMs);
});

if (fut.wait_for(std::chrono::seconds(2)) == std::future_status::ready) {
const int64_t durationMs = fut.get();
if (durationMs > 0 && durationMs > 15 * 1000) {
QFile::remove(destPath);
outError = obs_module_text("CustomizedCartoon.Error.VideoTooLong");
return false;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Probing the video duration synchronously on the Qt UI thread using fut.wait_for(std::chrono::seconds(2)) will freeze the user interface for up to 2 seconds. Furthermore, creating and releasing OBS sources (obs_source_create and obs_source_release) on a background thread (QThreadPool via ScheduleOBSTask) is unsafe and can lead to crashes or rendering issues in OBS. Consider probing the video metadata asynchronously or using a lightweight metadata parser that does not instantiate a full OBS source on a background thread.

// for server to actually start listening.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
server_thread_->setObjectName(QString("17live-http-%1").arg(QString::fromStdString(name_)));
QObject::connect(server_thread_, &QThread::finished, server_thread_, &QObject::deleteLater);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Connecting QThread::finished to QObject::deleteLater on a thread created via QThread::create while also calling wait(5000) and clearing the pointer in stop() can lead to a race condition. If the thread finishes and is deleted by the main thread's event loop while the background thread is waiting on it, a crash will occur. It is safer to avoid deleteLater and instead delete the QThread object manually in stop() after wait() returns.

Comment on lines +338 to +339
// Manual close keeps the dock instance around, so recreate the browser next time it opens.
cefWidget_.reset(nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling cefWidget_.reset(nullptr) immediately after cefWidget_->closeBrowser() deletes the QCefWidget synchronously. Since CEF browser destruction is asynchronous, deleting the widget before the browser is fully closed can lead to crashes or hangs. Consider using cefWidget_.take()->deleteLater() to safely defer the widget's deletion to the event loop.

Suggested change
// Manual close keeps the dock instance around, so recreate the browser next time it opens.
cefWidget_.reset(nullptr);
// Manual close keeps the dock instance around, so recreate the browser next time it opens.
cefWidget_.take()->deleteLater();

InstallDirRegKey HKLM "Software\OBS Studio" ""
ShowInstDetails show
ShowUnInstDetails show
InstallDir "$APPDATA\obs-studio\plugins"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a discrepancy between the installer code ($APPDATA\obs-studio\plugins) and the documentation/README (%ProgramData%\obs-studio\plugins). $APPDATA points to the user-specific AppData directory, whereas %ProgramData% is the global shared directory. If OBS is looking for plugins globally, installing to $APPDATA might prevent the plugin from loading, or vice versa. Additionally, if it is a user-specific install, RequestExecutionLevel admin should not be required.

Comment on lines 67 to 70
default:
return QString();
return res("bcc48ea8-606b-4e2d-ac46-230f68c23fd9.png");
// return QString();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning the level 11 badge as the default fallback in mLevelBadgeResource will cause any unhandled or unknown levels to display the level 11 badge, which is incorrect. It should return QString() or a generic default badge. Also, please remove the commented-out code.

    default:
        return QString();
    }

Comment on lines +72 to +73
bool GetUserNote(const std::string userID, OneSevenLiveUserNote &response);
bool SetUserNote(const std::string userID, const QString &content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing std::string by value (const std::string userID) in GetUserNote and SetUserNote causes unnecessary copies. Consider passing it by const reference (const std::string &userID) instead.

    bool GetUserNote(const std::string &userID, OneSevenLiveUserNote &response);
    bool SetUserNote(const std::string &userID, const QString &content);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants