feat: make everything threads-safe.

feat: complete `uninstall.sh` logic to make everything synced.
	feat: complete basic play logic to handle background, music,
	lyric play.
This commit is contained in:
2026-08-30 15:13:28 +08:00
parent 00b7c0b324
commit 7edba3fbc0
10 changed files with 355 additions and 65 deletions
+48 -18
View File
@@ -410,9 +410,17 @@ void renderRgbFrame(const RgbFrame& frame, float intensity, int offsetX, int off
// --- playback --------------------------------------------------------------
int playVideo(const std::string& path, const PlayVideoOptions& opts) {
int playVideo(const std::string& path, Timeline* timeline,
const PlayVideoOptions& opts) {
if (!isTerminal()) return 2;
// Fallback clock for callers that don't provide one.
Timeline localTimeline;
if (!timeline) {
timeline = &localTimeline;
timeline->start();
}
DecodeContext dc;
int videoW = 0, videoH = 0;
double fps = 24.0;
@@ -431,6 +439,7 @@ int playVideo(const std::string& path, const PlayVideoOptions& opts) {
int gridW = 0, gridH = 0;
int offsetX = 0, offsetY = 0;
bool showLabel = false;
const int topRows = opts.topRows > 0 ? opts.topRows : 0;
const int bottomRows = opts.bottomRows > 0 ? opts.bottomRows : 0;
bool adjustable = false;
@@ -438,7 +447,7 @@ int playVideo(const std::string& path, const PlayVideoOptions& opts) {
int aspectRows = static_cast<int>(std::lround(
ts.cols * static_cast<double>(videoH) / (2.0 * videoW)));
aspectRows = std::clamp(aspectRows, 2, 200);
adjustable = resizeTerminal(aspectRows + bottomRows, ts.cols);
adjustable = resizeTerminal(aspectRows + topRows + bottomRows, ts.cols);
if (adjustable) {
// The terminal changed; use whatever it actually became.
ts = terminalSize();
@@ -447,22 +456,30 @@ int playVideo(const std::string& path, const PlayVideoOptions& opts) {
if (adjustable) {
gridW = ts.cols;
gridH = std::max(1, ts.rows - bottomRows);
gridH = std::max(1, ts.rows - topRows - bottomRows);
offsetY = topRows;
} else {
// Not adjustable (or resize disabled): fit proportionally into the
// current size. The frame-size label gets its own line at the top, so
// reserve it before fitting; bottom rows are kept free for other
// content (e.g. the lyric line).
const int labelRows = 1;
const int maxRows = std::max(1, ts.rows - bottomRows - labelRows);
// current size. When the top is reserved for other content (lyrics),
// the frame-size label is suppressed; otherwise it gets its own line
// at the top.
showLabel = (topRows == 0);
const int labelRows = showLabel ? 1 : 0;
const int maxRows = std::max(1, ts.rows - topRows - bottomRows - labelRows);
GridSize g = fitSizeForAspect(videoW, videoH, ts.cols, maxRows);
gridW = g.cols;
gridH = g.rows;
showLabel = true; // print "WxH" on its own line at the top
offsetX = (ts.cols - gridW) / 2;
offsetY = 1 + (maxRows - gridH) / 2;
offsetY = topRows + (maxRows - gridH) / 2;
}
if (opts.outGrid) {
opts.outGrid->cols = gridW;
opts.outGrid->rows = gridH;
}
if (opts.outOffsetX) *opts.outOffsetX = offsetX;
if (opts.gridReady) opts.gridReady->store(true);
if (gridW <= 0 || gridH <= 0 || setupScaler(dc, gridW, gridH) < 0) {
freeDecode(dc);
return 1;
@@ -479,25 +496,38 @@ int playVideo(const std::string& path, const PlayVideoOptions& opts) {
dc.loop = opts.loop;
std::thread decoder(decoderThreadFn, std::ref(dc));
const auto interval =
std::chrono::microseconds(static_cast<int64_t>(1'000'000.0 / fps));
auto next = std::chrono::steady_clock::now();
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;
while (!(opts.stop && opts.stop->load())) {
RgbFrame frame;
if (queue.popLatestUntil(next, 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);
// Wait until the timeline reaches this slot (unless stopped).
while (timeline->seconds() < nextSlot) {
if (opts.stop && opts.stop->load()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (opts.stop && opts.stop->load()) break;
if (got) {
renderRgbFrame(frame, opts.intensity, offsetX, offsetY, showLabel, videoW,
videoH, out);
writeRaw(out);
}
if (finished.load() && queue.empty()) break;
next += interval;
const auto now = std::chrono::steady_clock::now();
if (next <= now) next = now + interval; // fell behind: re-anchor
std::this_thread::sleep_until(next);
const double now = timeline->seconds();
nextSlot = std::max(nextSlot + intervalSec, now + intervalSec);
}
queue.setFinished();