Skip to content

Commit a9fa98e

Browse files
committed
fix: resolve shutdown deadlock, unblock quit signal, and fix thread synchronization
1 parent 23e26e8 commit a9fa98e

12 files changed

Lines changed: 99 additions & 50 deletions

‎CHANGELOG.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ All notable changes to **StreamMatrix** will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
---
8+
## [1.0.1] - 2026-08-29
9+
10+
### Fixed
11+
- **Shutdown and Quit Execution**: Connected QML engine `quit` signal to `QGuiApplication::quit` and added window `onClosing` hook, resolving issues where the app ignored quit shortcuts and menu actions.
12+
- **Worker Thread Deadlock Prevention**: Fixed `requestInterrupt` across decoder waiting queues to properly wake blocked producer threads during shutdown and feed cancellation.
13+
- **Demuxer Loop Flood on Failure**: Corrected decode error propagation in the demuxer loop to prevent tight infinite loop spinning and event queue saturation.
14+
- **Thread Concurrency in `wait()`**: Synchronized worker thread completion waiting on condition variables to prevent concurrent `join()` data races.
15+
- **Video Buffer Plane Count**: Ensured planar video buffer returns accurate active plane counts to prevent out-of-bounds plane sampling in Qt Quick rendering.
16+
- **Context Double-Free**: Added pointer safety in `Context` destructor.
917

1018
## [1.0.0] - 2026-08-24
1119

‎CMakeLists.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.14)
22

33
set(VER_MAJ 1)
44
set(VER_MIN 0)
5-
set(VER_PAT 0)
5+
set(VER_PAT 1)
66
set(REL_STATUS "alpha")
77

88
set(APP_NAME "StreamMatrix")

‎src/RootWindow.qml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ ApplicationWindow {
2222
width: rootWindowSettings.width
2323
height: rootWindowSettings.height
2424

25+
onClosing: Qt.quit()
26+
2527
// Right-to-left User Interfaces support
2628
LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft
2729
LayoutMirroring.childrenInherit: true

‎src/context.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
Context::~Context()
55
{
6-
delete m_config;
6+
if (m_config) {
7+
delete m_config;
8+
m_config = nullptr;
9+
}
710
}
811

912
void Context::init()

‎src/main.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ int main(int argc, char *argv[])
9898
if (!obj && url == objUrl)
9999
QCoreApplication::exit(-1);
100100
}, Qt::QueuedConnection);
101+
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit);
101102
engine.load(url);
102103

103104
return app.exec();

‎src/qmlav/src/qmlavdecoder.cpp‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ QString QmlAVDecoder::name() const
8989
bool QmlAVDecoder::decodeAVPacket(const AVPacketPtr &avPacket)
9090
{
9191
if (isOpen()) {
92-
m_threadTask(this, avPacket);
93-
return true;
92+
return m_threadTask(this, avPacket);
9493
}
9594

9695
return false;

‎src/qmlav/src/qmlavdemuxer.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,13 @@ void QmlAVDemuxer::start()
160160
}
161161

162162
if (avPacket->stream_index == m_context->videoDecoder->streamIndex()) {
163-
m_context->videoDecoder->decodeAVPacket(avPacket) || stop();
163+
if (!m_context->videoDecoder->decodeAVPacket(avPacket)) {
164+
return stop();
165+
}
164166
} else if (avPacket->stream_index == m_context->audioDecoder->streamIndex()) {
165-
m_context->audioDecoder->decodeAVPacket(avPacket) || stop();
167+
if (!m_context->audioDecoder->decodeAVPacket(avPacket)) {
168+
return stop();
169+
}
166170
} else {
167171
return 1; // Minimal sleep time
168172
}

‎src/qmlav/src/qmlavdemuxer.h‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,49 @@ class QmlAVInterruptCallback : public AVIOInterruptCB
2222
QmlAVInterruptCallback() {
2323
opaque = this;
2424
callback = [](void *opaque) -> int {
25-
assert(opaque);
25+
if (!opaque) return 1;
2626
auto cb = static_cast<QmlAVInterruptCallback *>(opaque);
27-
return cb->isAVInterruptRequested() || (cb->m_expireTime > 0 && av_gettime_relative() > cb->m_expireTime);
27+
if (cb->isAVInterruptRequested()) return 1;
28+
int64_t expire = cb->m_expireTime.load(std::memory_order_relaxed);
29+
if (expire > 0 && av_gettime_relative() > expire) return 1;
30+
return 0;
2831
};
2932
}
3033

31-
void requestAVInterrupt() { m_avInterruptRequested.store(true, std::memory_order_relaxed); }
32-
bool isAVInterruptRequested() const { return m_avInterruptRequested.load(std::memory_order_relaxed); }
34+
QmlAVInterruptCallback(const QmlAVInterruptCallback &other) : AVIOInterruptCB() {
35+
opaque = this;
36+
callback = other.callback;
37+
m_timeout.store(other.m_timeout.load(std::memory_order_relaxed), std::memory_order_relaxed);
38+
m_expireTime.store(other.m_expireTime.load(std::memory_order_relaxed), std::memory_order_relaxed);
39+
m_avInterruptRequested.store(other.m_avInterruptRequested.load(std::memory_order_relaxed), std::memory_order_relaxed);
40+
}
41+
42+
QmlAVInterruptCallback &operator=(const QmlAVInterruptCallback &other) {
43+
if (this != &other) {
44+
opaque = this;
45+
callback = other.callback;
46+
m_timeout.store(other.m_timeout.load(std::memory_order_relaxed), std::memory_order_relaxed);
47+
m_expireTime.store(other.m_expireTime.load(std::memory_order_relaxed), std::memory_order_relaxed);
48+
m_avInterruptRequested.store(other.m_avInterruptRequested.load(std::memory_order_relaxed), std::memory_order_relaxed);
49+
}
50+
return *this;
51+
}
52+
53+
void requestAVInterrupt() { m_avInterruptRequested.store(true, std::memory_order_release); }
54+
bool isAVInterruptRequested() const { return m_avInterruptRequested.load(std::memory_order_acquire); }
3355

34-
// NOTE: Not thread safe!
3556
void setTimeout(int64_t timeout) {
36-
m_timeout = timeout;
57+
m_timeout.store(timeout, std::memory_order_relaxed);
3758
resetTimer();
3859
}
3960
void resetTimer() {
40-
m_expireTime = av_gettime_relative() + m_timeout;
61+
int64_t t = m_timeout.load(std::memory_order_relaxed);
62+
m_expireTime.store(t > 0 ? (av_gettime_relative() + t) : 0, std::memory_order_relaxed);
4163
}
4264

4365
private:
44-
int64_t m_timeout = 0;
45-
int64_t m_expireTime = 0;
66+
std::atomic<int64_t> m_timeout = 0;
67+
std::atomic<int64_t> m_expireTime = 0;
4668
std::atomic<bool> m_avInterruptRequested = false;
4769
};
4870

‎src/qmlav/src/qmlavthread.h‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class QmlAVWorkerResultImpl : public QmlAVAbstractWorker
6161
}
6262

6363
virtual void requestInterrupt() override {
64-
m_results.setConsumerLimit(0);
64+
m_results.requestInterrupt();
6565
}
6666

6767
protected:
@@ -173,7 +173,7 @@ class QmlAVWorker<Callable, std::shared_ptr<ArgsQueue>> : public QmlAVWorkerInvo
173173

174174
virtual void requestInterrupt() override final {
175175
if (m_argsQueue) {
176-
m_argsQueue->setConsumerLimit(0);
176+
m_argsQueue->requestInterrupt();
177177
}
178178

179179
__Super::requestInterrupt();
@@ -194,6 +194,9 @@ class QmlAVWorkerThread
194194
virtual ~QmlAVWorkerThread() {
195195
requestInterrupt();
196196
wait();
197+
if (m_thread.joinable() && std::this_thread::get_id() != m_thread.get_id()) {
198+
m_thread.join();
199+
}
197200
}
198201

199202
void start() {
@@ -202,8 +205,8 @@ class QmlAVWorkerThread
202205
}
203206
}
204207
void wait() {
205-
if (m_thread.joinable() && std::this_thread::get_id() != m_thread.get_id()) {
206-
m_thread.join();
208+
if (std::this_thread::get_id() == m_thread.get_id()) {
209+
return;
207210
}
208211
std::unique_lock<std::mutex> lock(m_mutex);
209212
m_waitCond.wait(lock, [&] {
@@ -355,8 +358,8 @@ class QmlAVThreadTask
355358
auto argsQueue() const { return m_argsQueue; }
356359

357360
template<typename ...URef>
358-
void operator() (URef &&...args) {
359-
m_argsQueue->enqueue(std::forward_as_tuple(args...));
361+
bool operator() (URef &&...args) {
362+
return m_argsQueue->enqueue(std::forward_as_tuple(args...));
360363
}
361364

362365
auto getLiveController() {

‎src/qmlav/src/qmlavvideobuffer.cpp‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,21 @@ int QmlAVVideoBuffer::map(QAbstractVideoBuffer::MapMode mode, int *numBytes, int
3030

3131
auto mapData = map(mode);
3232

33-
int i = 0;
34-
for (*numBytes = 0; i < QMLAV_NUM_DATA_POINTERS; ++i) {
35-
*numBytes += mapData.size[i];
36-
bytesPerLine[i] = mapData.bytesPerLine[i];
37-
data[i] = mapData.data[i];
33+
int planesCount = 0;
34+
*numBytes = 0;
35+
for (int i = 0; i < QMLAV_NUM_DATA_POINTERS; ++i) {
36+
if (mapData.data[i] && mapData.size[i] > 0) {
37+
*numBytes += mapData.size[i];
38+
bytesPerLine[i] = mapData.bytesPerLine[i];
39+
data[i] = mapData.data[i];
40+
planesCount = i + 1;
41+
} else {
42+
bytesPerLine[i] = 0;
43+
data[i] = nullptr;
44+
}
3845
}
3946

40-
return i;
47+
return planesCount;
4148
}
4249

4350
bool QmlAVVideoBuffer::planeSizes(int size[]) const

0 commit comments

Comments
 (0)