fix(backgroundDisplay): fix video frams loop in severial of

beginnings. Now the video can be played correctly.
	feat(build): add build script for Windows.
This commit is contained in:
2026-08-30 16:10:42 +08:00
parent 7edba3fbc0
commit 09efa15f1d
3 changed files with 126 additions and 27 deletions
+59
View File
@@ -0,0 +1,59 @@
@echo off
rem =====================================================================
rem build-win.bat - Build the project on Windows (MSYS2 UCRT64 + MinGW).
rem
rem Builds build\hatsune-miku-shoshitsu.exe and build\hatsune-miku-demo.exe
rem
rem Requirements (install once with MSYS2's pacman, using a proxy if needed):
rem pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gcc-libs ^
rem mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-cmake
rem
rem The FFmpeg dev libraries must be installed together with the gcc
rem toolchain (FFmpeg 9 needs newer gcc-libs than a stock older gcc).
rem =====================================================================
setlocal EnableDelayedExpansion
set "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%"
rem ---- locate the MSYS2 UCRT64 toolchain ----------------------------------
rem (note: an inherited MSYS2_ROOT env var is respected; don't pre-set it here
rem to empty, or "if not defined" below would never match)
if not defined MSYS2_ROOT if exist "D:\SDKs\msys64\ucrt64\bin\g++.exe" set "MSYS2_ROOT=D:\SDKs\msys64"
if not defined MSYS2_ROOT if exist "C:\msys64\ucrt64\bin\g++.exe" set "MSYS2_ROOT=C:\msys64"
if not defined MSYS2_ROOT (
echo [build-win] MSYS2 UCRT64 not found.
echo Set the MSYS2_ROOT environment variable, e.g.:
echo set MSYS2_ROOT=D:\SDK\msys64
exit /b 1
)
set "PATH=%MSYS2_ROOT%\ucrt64\bin;%PATH%"
echo [build-win] MSYS2 UCRT64 : %MSYS2_ROOT%
rem ---- sanity checks --------------------------------------------------------
where cmake.exe >nul 2>&1 || (
echo [build-win] cmake not found. Install mingw-w64-ucrt-x86_64-cmake.
exit /b 1
)
pkg-config --exists libavformat || (
echo [build-win] FFmpeg dev libraries not found.
echo Install mingw-w64-ucrt-x86_64-ffmpeg.
exit /b 1
)
rem ---- configure + build ----------------------------------------------------
cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
if errorlevel 1 exit /b 1
cmake --build build -j
if errorlevel 1 exit /b 1
echo.
echo [build-win] Done.
echo %SCRIPT_DIR%build\hatsune-miku-shoshitsu.exe
echo %SCRIPT_DIR%build\hatsune-miku-demo.exe
echo.
echo Run with the MSYS2 UCRT64 bin dir on PATH so the FFmpeg DLLs resolve:
echo set PATH=%MSYS2_ROOT%\ucrt64\bin;%%PATH%%
echo build\hatsune-miku-shoshitsu.exe --video ^<file^> --audio ^<file^> --lyrics ^<file^>
exit /b 0
+66 -26
View File
@@ -157,27 +157,35 @@ struct RgbFrame {
std::vector<uint8_t> rgb; // width * height * 3, tightly packed RGB24
};
// Bounded queue that always keeps the newest frame. pushLatest() never blocks
// (drops the oldest), so the producer can't deadlock against the consumer.
class BoundedQueue {
// Bounded FIFO of decoded frames. The producer BLOCKS while the queue is full
// (backpressure), so it can never run far ahead of the consumer; the consumer
// pops frames in decode order and plays them at the timeline pace. Without
// this, a decoder that is faster than real time would let the renderer keep
// showing the "latest" frame and the video would fast-forward / loop-jump.
class FrameQueue {
public:
explicit BoundedQueue(size_t capacity) : capacity_(capacity) {}
explicit FrameQueue(size_t capacity) : capacity_(capacity) {}
void pushLatest(RgbFrame frame) {
std::lock_guard<std::mutex> lock(mutex_);
while (queue_.size() >= capacity_) queue_.pop_front();
// Producer: blocks while full. Returns false if the queue was closed
// (playback ended), telling the decoder to stop.
bool push(RgbFrame frame) {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return queue_.size() < capacity_ || closed_; });
if (closed_) return false;
queue_.push_back(std::move(frame));
cv_.notify_one();
cv_.notify_all();
return true;
}
// Wait up to `deadline` for the newest frame. Returns false if none
// arrived by then. Drains older frames (only the newest is kept).
bool popLatestUntil(std::chrono::steady_clock::time_point deadline, RgbFrame& out) {
// Consumer: wait up to `deadline` for the next frame (oldest first).
// Returns false if none arrived by then.
bool popUntil(std::chrono::steady_clock::time_point deadline, RgbFrame& out) {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_until(lock, deadline, [this] { return !queue_.empty() || finished_; });
cv_.wait_until(lock, deadline, [this] { return !queue_.empty() || finished_ || closed_; });
if (queue_.empty()) return false;
out = std::move(queue_.back());
queue_.clear();
out = std::move(queue_.front());
queue_.pop_front();
cv_.notify_all();
return true;
}
@@ -192,12 +200,21 @@ public:
cv_.notify_all();
}
// Unblock a waiting producer when playback is ending (called by the
// consumer right before it joins the decoder thread).
void close() {
std::lock_guard<std::mutex> lock(mutex_);
closed_ = true;
cv_.notify_all();
}
private:
mutable std::mutex mutex_;
std::condition_variable cv_;
std::deque<RgbFrame> queue_;
size_t capacity_;
bool finished_ = false;
bool closed_ = false;
};
struct DecodeContext {
@@ -218,8 +235,13 @@ struct DecodeContext {
AVFrame* frame = nullptr;
AVPacket* pkt = nullptr;
// Discard this many leading frames so playback starts at the timeline's
// current position (otherwise the video would lag the audio by the setup
// time spent probing the file and sizing the terminal).
int skipFrames = 0;
bool loop = false;
BoundedQueue* queue = nullptr;
FrameQueue* queue = nullptr;
const std::atomic<bool>* stop = nullptr;
std::atomic<bool>* finished = nullptr;
};
@@ -292,14 +314,21 @@ int setupScaler(DecodeContext& dc, int dstW, int dstH) {
return (dc.frame && dc.pkt) ? 0 : -1;
}
void scaleAndPush(DecodeContext& dc) {
// Scale the current frame and push it onto the queue. Skips the first
// `skipFrames` frames (start-sync). Returns false when the queue was closed,
// telling the decoder to stop.
bool scaleAndPush(DecodeContext& dc) {
if (dc.skipFrames > 0) {
--dc.skipFrames; // discard without scaling
return true;
}
sws_scale(dc.sws, dc.frame->data, dc.frame->linesize, 0, dc.frame->height,
dc.dstData, dc.dstLinesize);
RgbFrame frame;
frame.width = dc.dstW;
frame.height = dc.dstH;
frame.rgb.assign(dc.dstBuf.begin(), dc.dstBuf.end());
dc.queue->pushLatest(std::move(frame));
return dc.queue->push(std::move(frame));
}
void decoderThreadFn(DecodeContext& dc) {
@@ -308,7 +337,10 @@ void decoderThreadFn(DecodeContext& dc) {
if (r == AVERROR_EOF) {
// Flush whatever is buffered in the decoder.
avcodec_send_packet(dc.codec, nullptr);
while (avcodec_receive_frame(dc.codec, dc.frame) == 0) scaleAndPush(dc);
bool ok = true;
while (ok && avcodec_receive_frame(dc.codec, dc.frame) == 0)
ok = scaleAndPush(dc);
if (!ok) break;
if (dc.loop && !(dc.stop && dc.stop->load())) {
av_seek_frame(dc.fmt, -1, 0, AVSEEK_FLAG_BACKWARD);
@@ -320,7 +352,10 @@ void decoderThreadFn(DecodeContext& dc) {
if (r < 0) break; // non-EOF read error
if (dc.pkt->stream_index == dc.videoIndex) {
avcodec_send_packet(dc.codec, dc.pkt);
while (avcodec_receive_frame(dc.codec, dc.frame) == 0) scaleAndPush(dc);
bool ok = true;
while (ok && avcodec_receive_frame(dc.codec, dc.frame) == 0)
ok = scaleAndPush(dc);
if (!ok) break;
}
av_packet_unref(dc.pkt);
}
@@ -489,28 +524,33 @@ int playVideo(const std::string& path, Timeline* timeline,
writeRaw("\x1b[?25l\x1b[2J\x1b[H");
std::atomic<bool> finished(false);
BoundedQueue queue(4);
FrameQueue queue(8);
dc.queue = &queue;
dc.stop = opts.stop;
dc.finished = &finished;
dc.loop = opts.loop;
// Start the playback at the timeline's current position so the video
// doesn't lag the audio by the probe/setup time.
dc.skipFrames = std::max(0, static_cast<int>(timeline->seconds() * fps));
std::thread decoder(decoderThreadFn, std::ref(dc));
const double intervalSec = 1.0 / fps;
std::string out;
out.reserve(static_cast<size_t>(gridW) * gridH * 24);
// Render one (latest) frame per time slot, paced by the shared timeline.
// `nextSlot` jumps past any missed slots, so a slow decode never causes a
// catch-up burst of redraws.
double nextSlot = 0.0;
// Render frames in decode order, one per time slot paced by the shared
// timeline. `nextSlot` starts at the current position (frames we skipped
// in the decoder) and re-anchors to the current time when the renderer
// falls behind, so it never bursts a catch-up redraw; the blocking queue
// keeps the decoder from running ahead of us.
double nextSlot = timeline->seconds();
while (!(opts.stop && opts.stop->load())) {
RgbFrame frame;
const auto deadline =
timeline->startTime() +
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(nextSlot));
const bool got = queue.popLatestUntil(deadline, frame);
const bool got = queue.popUntil(deadline, frame);
// Wait until the timeline reaches this slot (unless stopped).
while (timeline->seconds() < nextSlot) {
@@ -530,7 +570,7 @@ int playVideo(const std::string& path, Timeline* timeline,
nextSlot = std::max(nextSlot + intervalSec, now + intervalSec);
}
queue.setFinished();
queue.close(); // unblock a producer stuck on a full queue, then join
decoder.join();
writeRaw("\x1b[0m\x1b[?25h");
freeDecode(dc);
+1 -1
View File
@@ -57,7 +57,7 @@ static int runShow(const std::string& videoPath, const std::string& audioPath,
bd::PlayVideoOptions vo;
vo.loop = true;
vo.intensity = 0.6f; // 60% brightness
vo.intensity = 1f; // brightness settings
vo.topRows = 1; // one row above the video reserved for the lyrics
vo.outGrid = &videoGrid;
vo.outOffsetX = &videoOffsetX;