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