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:
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user