mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-20 22:01:26 +02:00
- Add ThumbnailCache: a worker thread extracts 256x144 thumbnails and media metadata with FFmpeg, SBS-aware cropping, and an on-disk cache under ~/.cache/pixelvr/thumbs. - Draw thumbnails on browser cards through a new rounded-corner thumb.frag pipeline with per-card descriptor sets. - Redesign the browser as a sidebar + card grid, with horizontal stick navigation and direct shortcuts to Network/Home/Videos/Downloads/Drives. - Expand Settings: screen curvature, UI text scale and colour, accent colour and UI distance, organised into sections. - Downloads window: per-job state, progress, speed and ETA, plus pause/resume and cancel-active controls. - Decoder: multi-threaded software decode and four V4L2 output buffers. - docs: unified UI concept, redesign plan and mockups. Release packaging is now reproducible: tools/run.sh is the shipped launcher, tools/package-release.sh assembles the tarball, and a GitHub Actions workflow builds it natively on ARM64 for every v* tag. Stop committing release tarballs; they are attached to releases instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1252 lines
48 KiB
C++
1252 lines
48 KiB
C++
#include "media/media_engine.hpp"
|
|
|
|
#include "util/logging.hpp"
|
|
#include "util/smb_lock.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavformat/avformat.h>
|
|
#include <libavformat/avio.h>
|
|
#include <libavutil/channel_layout.h>
|
|
#include <libavutil/imgutils.h>
|
|
#include <libavutil/hwcontext_drm.h>
|
|
#include <libavutil/mem.h>
|
|
#include <libavutil/opt.h>
|
|
#include <libavutil/pixdesc.h>
|
|
#include <libswresample/swresample.h>
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
#include <pulse/error.h>
|
|
#include <pulse/simple.h>
|
|
#endif
|
|
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
#include <libsmbclient.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <cerrno>
|
|
#endif
|
|
|
|
namespace pixelvr {
|
|
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
namespace {
|
|
|
|
void smb_auth(SMBCCTX*, const char*, const char*, char*, int, char* un, int unlen,
|
|
char* pw, int pwlen) {
|
|
if (un != nullptr && unlen > 0) un[0] = '\0'; // guest / anonymous
|
|
if (pw != nullptr && pwlen > 0) pw[0] = '\0';
|
|
}
|
|
|
|
// Streams an smb:// URL through libsmbclient behind a FFmpeg AVIOContext. A
|
|
// background thread prefetches the file into a ring buffer so the demux read
|
|
// callback serves from memory; network jitter then no longer stalls decoding.
|
|
// Only the prefetch thread touches the SMB handle (read/lseek); the AVIO
|
|
// callbacks only touch the ring and request seeks.
|
|
struct SmbReadAhead {
|
|
static constexpr std::size_t kCapacity = 64u << 20; // 64 MiB cushion
|
|
static constexpr std::size_t kChunk = 256u << 10; // 256 KiB network reads
|
|
|
|
SMBCCTX* ctx = nullptr;
|
|
SMBCFILE* file = nullptr;
|
|
smbc_read_fn readFn = nullptr;
|
|
smbc_lseek_fn lseekFn = nullptr;
|
|
smbc_close_fn closeFn = nullptr;
|
|
int64_t fileSize = -1;
|
|
|
|
std::mutex mutex;
|
|
std::condition_variable dataCv; // data available / eof / stop
|
|
std::condition_variable spaceCv; // space available / seek / stop
|
|
std::thread thread;
|
|
std::vector<uint8_t> ring;
|
|
std::size_t head = 0; // ring index of the next byte to read out
|
|
std::size_t fill = 0; // bytes currently buffered
|
|
int64_t consumePos = 0; // absolute offset of the next consumer byte
|
|
int64_t prefetchPos = 0; // absolute offset the prefetch thread reads from
|
|
bool pendingSeek = false;
|
|
bool eof = false;
|
|
bool stop = false;
|
|
|
|
bool start(SMBCCTX* c, SMBCFILE* f) {
|
|
ctx = c;
|
|
file = f;
|
|
readFn = smbc_getFunctionRead(ctx);
|
|
lseekFn = smbc_getFunctionLseek(ctx);
|
|
closeFn = smbc_getFunctionClose(ctx);
|
|
smbc_fstat_fn fstatFn = smbc_getFunctionFstat(ctx);
|
|
struct stat st{};
|
|
fileSize = (fstatFn != nullptr && fstatFn(ctx, file, &st) == 0)
|
|
? static_cast<int64_t>(st.st_size)
|
|
: -1;
|
|
head = fill = 0;
|
|
consumePos = prefetchPos = 0;
|
|
pendingSeek = eof = stop = false;
|
|
ring.assign(kCapacity, 0);
|
|
thread = std::thread([this] { run(); });
|
|
return true;
|
|
}
|
|
|
|
void run() {
|
|
int64_t pos = 0;
|
|
std::vector<uint8_t> chunk(kChunk);
|
|
for (;;) {
|
|
std::size_t want;
|
|
bool doSeek = false;
|
|
int64_t seekPos = 0;
|
|
{
|
|
std::unique_lock<std::mutex> lk(mutex);
|
|
spaceCv.wait(lk, [this] {
|
|
return stop || pendingSeek || (!eof && fill < ring.size());
|
|
});
|
|
if (stop) return;
|
|
if (pendingSeek) {
|
|
pendingSeek = false;
|
|
head = fill = 0;
|
|
eof = false;
|
|
pos = prefetchPos;
|
|
doSeek = true;
|
|
seekPos = pos;
|
|
}
|
|
if (eof) continue;
|
|
want = std::min(ring.size() - fill, chunk.size());
|
|
}
|
|
// libsmbclient is not thread-safe; serialize the network syscalls
|
|
// against the browser's scans/downloads (per chunk, so they interleave).
|
|
if (doSeek) {
|
|
std::lock_guard<std::mutex> smb(smbGlobalLock());
|
|
lseekFn(ctx, file, static_cast<off_t>(seekPos), SEEK_SET);
|
|
}
|
|
if (want == 0) continue;
|
|
ssize_t n;
|
|
{
|
|
std::lock_guard<std::mutex> smb(smbGlobalLock());
|
|
n = readFn(ctx, file, chunk.data(), want);
|
|
}
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
if (stop || pendingSeek) continue; // a seek/stop superseded this read
|
|
if (n <= 0) {
|
|
eof = true;
|
|
dataCv.notify_all();
|
|
continue;
|
|
}
|
|
const std::size_t w = (head + fill) % ring.size();
|
|
const std::size_t first = std::min(static_cast<std::size_t>(n), ring.size() - w);
|
|
std::memcpy(ring.data() + w, chunk.data(), first);
|
|
if (static_cast<std::size_t>(n) > first) {
|
|
std::memcpy(ring.data(), chunk.data() + first,
|
|
static_cast<std::size_t>(n) - first);
|
|
}
|
|
fill += static_cast<std::size_t>(n);
|
|
pos += n;
|
|
prefetchPos = pos;
|
|
dataCv.notify_all();
|
|
}
|
|
}
|
|
|
|
int read(uint8_t* buf, int size) {
|
|
if (size <= 0) return 0;
|
|
std::unique_lock<std::mutex> lk(mutex);
|
|
dataCv.wait(lk, [this] { return stop || fill > 0 || eof; });
|
|
if (stop) return AVERROR_EXIT;
|
|
if (fill == 0) return AVERROR_EOF;
|
|
const std::size_t n = std::min(static_cast<std::size_t>(size), fill);
|
|
const std::size_t first = std::min(n, ring.size() - head);
|
|
std::memcpy(buf, ring.data() + head, first);
|
|
if (n > first) std::memcpy(buf + first, ring.data(), n - first);
|
|
head = (head + n) % ring.size();
|
|
fill -= n;
|
|
consumePos += static_cast<int64_t>(n);
|
|
spaceCv.notify_all();
|
|
return static_cast<int>(n);
|
|
}
|
|
|
|
int64_t seek(int64_t offset, int whence) {
|
|
if ((whence & ~AVSEEK_FORCE) == AVSEEK_SIZE) return fileSize;
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
const int w = whence & ~AVSEEK_FORCE;
|
|
int64_t target;
|
|
if (w == SEEK_SET) target = offset;
|
|
else if (w == SEEK_CUR) target = consumePos + offset;
|
|
else if (w == SEEK_END) target = (fileSize >= 0) ? fileSize + offset : -1;
|
|
else return -1;
|
|
if (target < 0) return -1;
|
|
consumePos = prefetchPos = target;
|
|
head = fill = 0; // discard data buffered from the old position
|
|
eof = false;
|
|
pendingSeek = true;
|
|
spaceCv.notify_all();
|
|
dataCv.notify_all();
|
|
return target;
|
|
}
|
|
|
|
// Unblocks a consumer stalled in read() so the decode thread can exit; the
|
|
// prefetch thread is joined later in shutdown().
|
|
void abort() {
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
stop = true;
|
|
dataCv.notify_all();
|
|
spaceCv.notify_all();
|
|
}
|
|
|
|
void shutdown() {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
stop = true;
|
|
dataCv.notify_all();
|
|
spaceCv.notify_all();
|
|
}
|
|
if (thread.joinable()) thread.join();
|
|
{
|
|
std::lock_guard<std::mutex> smb(smbGlobalLock());
|
|
if (file != nullptr && closeFn != nullptr) closeFn(ctx, file);
|
|
if (ctx != nullptr) smbc_free_context(ctx, 1);
|
|
}
|
|
ctx = nullptr;
|
|
file = nullptr;
|
|
ring.clear();
|
|
ring.shrink_to_fit();
|
|
}
|
|
};
|
|
|
|
int smb_read(void* opaque, uint8_t* buf, int size) {
|
|
return static_cast<SmbReadAhead*>(opaque)->read(buf, size);
|
|
}
|
|
|
|
int64_t smb_seek(void* opaque, int64_t offset, int whence) {
|
|
return static_cast<SmbReadAhead*>(opaque)->seek(offset, whence);
|
|
}
|
|
|
|
} // namespace
|
|
#endif
|
|
|
|
// Playback clock in the VIDEO PTS timeline. It starts when the first frame is
|
|
// presented and advances with wall time, so consumer-side selection is smooth at
|
|
// any frame rate and independent of the audio PTS epoch. It freezes while paused
|
|
// and resets on seek. Frame production stays audio-paced, so the bounded frame
|
|
// queue keeps video locked to audio without assuming a shared PTS epoch.
|
|
struct PlaybackClock {
|
|
std::atomic<double> basePts{0.0};
|
|
std::atomic<int64_t> baseWallNs{0}; // steady_clock ns; 0 = stopped/frozen
|
|
std::atomic<bool> running{false};
|
|
|
|
static int64_t nowNs() {
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch())
|
|
.count();
|
|
}
|
|
void start(double pts) {
|
|
basePts.store(pts);
|
|
baseWallNs.store(nowNs());
|
|
running.store(true);
|
|
}
|
|
void freeze() {
|
|
if (!running.exchange(false)) return;
|
|
const int64_t w = baseWallNs.exchange(0);
|
|
if (w != 0) {
|
|
basePts.store(basePts.load() + static_cast<double>(nowNs() - w) / 1e9);
|
|
}
|
|
}
|
|
void resume() {
|
|
if (running.exchange(true)) return;
|
|
baseWallNs.store(nowNs());
|
|
}
|
|
void reset() {
|
|
running.store(false);
|
|
baseWallNs.store(0);
|
|
basePts.store(0.0);
|
|
}
|
|
double now() const {
|
|
const int64_t w = baseWallNs.load();
|
|
if (w == 0) return basePts.load();
|
|
return basePts.load() + static_cast<double>(nowNs() - w) / 1e9;
|
|
}
|
|
};
|
|
|
|
struct MediaEngine::Impl {
|
|
AVFormatContext* format = nullptr;
|
|
AVCodecContext* codec = nullptr;
|
|
SwsContext* sws = nullptr;
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
AVIOContext* avio = nullptr;
|
|
SmbReadAhead smbReader;
|
|
#endif
|
|
int videoStream = -1;
|
|
int width = 0;
|
|
int height = 0;
|
|
double duration = 0.0;
|
|
double frameRate = 0.0;
|
|
AVRational timeBase{1, 1};
|
|
int drmDiag = 0;
|
|
std::string codecName;
|
|
const AVCodec* videoDecoder = nullptr; // retained for reopen-on-seek
|
|
bool decoderDrm = false; // apply drm_prime opts on reopen
|
|
int captureBuffers = 0; // num_capture_buffers for reopen
|
|
std::atomic<uint64_t> decodedCount{0};
|
|
|
|
std::vector<MediaEngine::StreamInfo> videoStreamList;
|
|
std::vector<MediaEngine::StreamInfo> audioStreamList;
|
|
int preferredVideoStream = -1; // container index to force, or -1 = auto
|
|
int preferredAudioStream = -1;
|
|
std::string preferredLanguage; // ISO code to auto-pick audio, or empty
|
|
|
|
int audioStream = -1;
|
|
AVCodecContext* audioCodec = nullptr;
|
|
SwrContext* swr = nullptr;
|
|
AVRational audioTimeBase{1, 1};
|
|
bool audioEnabled = false;
|
|
static constexpr int kOutSampleRate = 48000;
|
|
static constexpr int kOutChannels = 2;
|
|
std::atomic<double> masterClock{0.0};
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
pa_simple* pa = nullptr;
|
|
#endif
|
|
|
|
std::thread thread;
|
|
std::atomic<bool> running{false};
|
|
std::atomic<bool> eof{false};
|
|
std::atomic<bool> paused{false};
|
|
std::atomic<double> seekTarget{-1.0}; // >= 0 requests a seek (seconds)
|
|
std::atomic<bool> releaseReq{false}; // ask the consumer to free held frames
|
|
std::condition_variable releaseCv; // consumer signals release complete
|
|
std::condition_variable queueCv; // a queue slot freed / seek / stop
|
|
|
|
// Decoded video frames awaiting display, ordered by PTS. The render thread
|
|
// selects the frame matching the master clock each vsync (dropping late
|
|
// frames, holding early ones) so presentation is phase-locked to the
|
|
// compositor instead of the decode cadence. Audio still paces the decode
|
|
// loop and drives masterClock; video is scheduled on the consumer side.
|
|
std::mutex mutex;
|
|
std::deque<VideoFrame> swQueue; // CPU frames, guarded by mutex
|
|
std::deque<DrmVideoFrame> drmQueue; // dma-buf frames, guarded by mutex
|
|
uint64_t serial = 0; // guarded by mutex; last frame id
|
|
bool primed = false; // guarded by mutex; first frame shown?
|
|
static constexpr std::size_t kMaxQueue = 6;
|
|
std::size_t maxQueue = kMaxQueue; // effective cap; reduced for huge frames
|
|
std::atomic<bool> usingDrm{false};
|
|
PlaybackClock playClock; // video-epoch clock, anchored on the first frame
|
|
|
|
~Impl() { teardown(); }
|
|
|
|
void teardown() {
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
if (pa) {
|
|
pa_simple_free(pa);
|
|
pa = nullptr;
|
|
}
|
|
#endif
|
|
if (swr) {
|
|
swr_free(&swr);
|
|
}
|
|
if (audioCodec) {
|
|
avcodec_free_context(&audioCodec);
|
|
}
|
|
if (sws) {
|
|
sws_freeContext(sws);
|
|
sws = nullptr;
|
|
}
|
|
if (codec) {
|
|
avcodec_free_context(&codec);
|
|
}
|
|
if (format) {
|
|
avformat_close_input(&format);
|
|
}
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
if (avio != nullptr) {
|
|
av_freep(&avio->buffer);
|
|
avio_context_free(&avio);
|
|
}
|
|
smbReader.shutdown();
|
|
#endif
|
|
clearQueues();
|
|
maxQueue = kMaxQueue;
|
|
usingDrm.store(false);
|
|
audioEnabled = false;
|
|
audioStream = -1;
|
|
}
|
|
|
|
// Drops every queued frame, closing any dma-buf handles they own. The caller
|
|
// holds `mutex` when the decode thread is running (seek); teardown runs after
|
|
// the thread has joined, so no lock is needed there.
|
|
void clearQueues() {
|
|
for (DrmVideoFrame& f : drmQueue) {
|
|
if (f.fd >= 0) ::close(f.fd);
|
|
}
|
|
drmQueue.clear();
|
|
swQueue.clear();
|
|
primed = false;
|
|
}
|
|
|
|
// Playback position in seconds for consumer-side frame selection, in the
|
|
// video PTS timeline (anchored to the first presented frame).
|
|
double currentClock() const {
|
|
return playClock.now();
|
|
}
|
|
|
|
void pushFrame(int w, int h, std::vector<uint8_t>&& y, std::vector<uint8_t>&& uv,
|
|
int colorspace, bool fullRange, double pts) {
|
|
std::unique_lock<std::mutex> lock(mutex);
|
|
// Back-pressure: wait for the consumer to free a slot rather than dropping
|
|
// frames, so the decoder is paced by presentation and cannot race ahead of
|
|
// the playback clock. An unpaced decode burst (post-seek audio mute, or the
|
|
// initial burst on a fresh video) otherwise outran the clock and froze the
|
|
// picture permanently.
|
|
queueCv.wait_for(lock, std::chrono::milliseconds(100), [&] {
|
|
return swQueue.size() < maxQueue || !running.load() ||
|
|
seekTarget.load() >= 0.0;
|
|
});
|
|
if (!running.load() || seekTarget.load() >= 0.0) {
|
|
return; // a stop/seek is pending: drop this stale frame
|
|
}
|
|
while (swQueue.size() >= maxQueue) {
|
|
swQueue.pop_front(); // consumer stalled past the timeout: stay live
|
|
}
|
|
VideoFrame f;
|
|
f.width = w;
|
|
f.height = h;
|
|
f.y = std::move(y);
|
|
f.uv = std::move(uv);
|
|
f.colorspace = colorspace;
|
|
f.fullRange = fullRange;
|
|
f.pts = pts;
|
|
f.serial = ++serial;
|
|
swQueue.push_back(std::move(f));
|
|
}
|
|
|
|
void pushDrm(const DrmVideoFrame& f) {
|
|
std::unique_lock<std::mutex> lock(mutex);
|
|
// Back-pressure (see pushFrame): pace the decoder to the consumer so a
|
|
// post-seek or startup decode burst can't outrun the playback clock.
|
|
queueCv.wait_for(lock, std::chrono::milliseconds(100), [&] {
|
|
return drmQueue.size() < maxQueue || !running.load() ||
|
|
seekTarget.load() >= 0.0;
|
|
});
|
|
if (!running.load() || seekTarget.load() >= 0.0) {
|
|
if (f.fd >= 0) ::close(f.fd); // stop/seek pending: drop; we own the fd
|
|
return;
|
|
}
|
|
while (drmQueue.size() >= maxQueue) {
|
|
if (drmQueue.front().fd >= 0) ::close(drmQueue.front().fd);
|
|
drmQueue.pop_front(); // consumer stalled past the timeout: stay live
|
|
}
|
|
DrmVideoFrame queued = f;
|
|
queued.serial = ++serial;
|
|
drmQueue.push_back(std::move(queued));
|
|
usingDrm.store(true);
|
|
}
|
|
|
|
// Reopens the video decoder from scratch (matineevr's seek strategy): a flushed
|
|
// V4L2 hardware decoder can wedge under rapid seeking, but a fresh one cannot.
|
|
bool reopenDecoder() {
|
|
if (codec != nullptr) {
|
|
avcodec_free_context(&codec);
|
|
}
|
|
if (videoDecoder == nullptr || format == nullptr) return false;
|
|
AVStream* s = format->streams[videoStream];
|
|
codec = avcodec_alloc_context3(videoDecoder);
|
|
if (codec == nullptr ||
|
|
avcodec_parameters_to_context(codec, s->codecpar) < 0) {
|
|
return false;
|
|
}
|
|
codec->pkt_timebase = s->time_base;
|
|
codec->thread_count = 0; // Multi-threaded SW decode across all CPU cores
|
|
codec->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
|
|
if (std::strstr(videoDecoder->name, "v4l2m2m") != nullptr) {
|
|
av_opt_set_int(codec->priv_data, "num_output_buffers", 4, 0);
|
|
}
|
|
if (decoderDrm) {
|
|
av_opt_set_int(codec->priv_data, "num_capture_buffers", captureBuffers, 0);
|
|
av_opt_set_int(codec->priv_data, "drm_prime", 1, 0);
|
|
codec->apply_cropping = 0;
|
|
}
|
|
return avcodec_open2(codec, videoDecoder, nullptr) >= 0;
|
|
}
|
|
|
|
void decodeLoop();
|
|
};
|
|
|
|
void MediaEngine::Impl::decodeLoop() {
|
|
AVPacket* packet = av_packet_alloc();
|
|
AVFrame* frame = av_frame_alloc();
|
|
if (packet == nullptr || frame == nullptr) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: failed to allocate FFmpeg frame/packet");
|
|
av_packet_free(&packet);
|
|
av_frame_free(&frame);
|
|
return;
|
|
}
|
|
|
|
using clock = std::chrono::steady_clock;
|
|
clock::time_point startTime{};
|
|
double startPts = 0.0;
|
|
bool haveClock = false;
|
|
bool eofFlushed = false;
|
|
auto lastSeekWall = clock::now() - std::chrono::seconds(1);
|
|
|
|
auto process_video = [&]() {
|
|
decodedCount.fetch_add(1, std::memory_order_relaxed);
|
|
const int w = frame->width;
|
|
const int h = frame->height;
|
|
const auto srcFmt = static_cast<AVPixelFormat>(frame->format);
|
|
const int colorspace = static_cast<int>(frame->colorspace);
|
|
const bool fullRange = frame->color_range == AVCOL_RANGE_JPEG;
|
|
|
|
int64_t ts = frame->best_effort_timestamp;
|
|
if (ts == AV_NOPTS_VALUE) {
|
|
ts = frame->pts;
|
|
}
|
|
const double pts = (ts == AV_NOPTS_VALUE) ? 0.0 : ts * av_q2d(timeBase);
|
|
|
|
const bool isDrm =
|
|
srcFmt == AV_PIX_FMT_DRM_PRIME && frame->data[0] != nullptr;
|
|
DrmVideoFrame drm;
|
|
std::vector<uint8_t> y, uv;
|
|
|
|
if (isDrm) {
|
|
const auto* desc =
|
|
reinterpret_cast<const AVDRMFrameDescriptor*>(frame->data[0]);
|
|
const AVDRMLayerDescriptor& layer = desc->layers[0];
|
|
drm.fd = ::dup(desc->objects[0].fd);
|
|
if (drm.fd < 0) {
|
|
return; // could not keep the dma-buf alive across the handoff
|
|
}
|
|
drm.width = frame->width;
|
|
drm.height = frame->height;
|
|
drm.fourcc = layer.format;
|
|
drm.modifier = desc->objects[0].format_modifier;
|
|
drm.size = static_cast<int64_t>(desc->objects[0].size);
|
|
drm.offset[0] = static_cast<int>(layer.planes[0].offset);
|
|
drm.offset[1] = static_cast<int>(layer.planes[1].offset);
|
|
drm.pitch[0] = static_cast<int>(layer.planes[0].pitch);
|
|
drm.pitch[1] = static_cast<int>(layer.planes[1].pitch);
|
|
drm.crop[0] = frame->crop_left;
|
|
drm.crop[1] = frame->crop_top;
|
|
drm.crop[2] = frame->crop_right;
|
|
drm.crop[3] = frame->crop_bottom;
|
|
drm.colorspace = colorspace;
|
|
drm.fullRange = fullRange;
|
|
drm.pts = pts;
|
|
// Keep the decoder's frame referenced until the GPU is done with
|
|
// this dma-buf; otherwise the V4L2 buffer is re-queued and the
|
|
// decoder overwrites it mid-import (stale handle / GPU fault).
|
|
if (AVFrame* hold = av_frame_clone(frame)) {
|
|
drm.keepAlive = std::shared_ptr<void>(hold, [](void* p) {
|
|
AVFrame* f = static_cast<AVFrame*>(p);
|
|
av_frame_free(&f);
|
|
});
|
|
}
|
|
if (drmDiag < 2) {
|
|
++drmDiag;
|
|
PIXELVR_LOG_INFO(
|
|
"drm frame: %dx%d crop %d,%d,%d,%d fd=%d size=%ld mod=%#lx "
|
|
"fourcc=%c%c%c%c p0(%d,%d) p1(%d,%d)",
|
|
drm.width, drm.height, drm.crop[0], drm.crop[1], drm.crop[2],
|
|
drm.crop[3], drm.fd, static_cast<long>(drm.size),
|
|
static_cast<unsigned long>(drm.modifier),
|
|
static_cast<char>(drm.fourcc & 0xff),
|
|
static_cast<char>((drm.fourcc >> 8) & 0xff),
|
|
static_cast<char>((drm.fourcc >> 16) & 0xff),
|
|
static_cast<char>((drm.fourcc >> 24) & 0xff), drm.offset[0],
|
|
drm.pitch[0], drm.offset[1], drm.pitch[1]);
|
|
}
|
|
} else {
|
|
y.assign(static_cast<std::size_t>(w) * h, 0);
|
|
uv.assign(static_cast<std::size_t>(w) * (h / 2), 0);
|
|
if (srcFmt == AV_PIX_FMT_NV12) {
|
|
// Already NV12 (V4L2 hardware, line-padded to an aligned height).
|
|
// Repack tightly: libswscale's NV12->NV12 drops chroma here.
|
|
const uint8_t* srcY = frame->data[0];
|
|
const uint8_t* srcUV = frame->data[1];
|
|
const std::size_t lsY = static_cast<std::size_t>(frame->linesize[0]);
|
|
const std::size_t lsUV = static_cast<std::size_t>(frame->linesize[1]);
|
|
for (int row = 0; row < h; ++row) {
|
|
std::memcpy(y.data() + static_cast<std::size_t>(row) * w,
|
|
srcY + static_cast<std::size_t>(row) * lsY, w);
|
|
}
|
|
for (int row = 0; row < h / 2; ++row) {
|
|
std::memcpy(uv.data() + static_cast<std::size_t>(row) * w,
|
|
srcUV + static_cast<std::size_t>(row) * lsUV, w);
|
|
}
|
|
} else {
|
|
// Convert other outputs (e.g. software yuv420p) to NV12.
|
|
sws = sws_getCachedContext(sws, w, h, srcFmt, w, h, AV_PIX_FMT_NV12,
|
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
if (sws == nullptr) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: sws_getCachedContext failed");
|
|
return;
|
|
}
|
|
uint8_t* dstData[4] = {y.data(), uv.data(), nullptr, nullptr};
|
|
int dstLinesize[4] = {w, w, 0, 0};
|
|
sws_scale(sws, frame->data, frame->linesize, 0, h, dstData,
|
|
dstLinesize);
|
|
}
|
|
}
|
|
|
|
// With audio, the blocking audio writes pace the loop and provide the
|
|
// master clock; without audio, pace video to its own PTS via wall clock.
|
|
if (!audioEnabled) {
|
|
if (!haveClock) {
|
|
startTime = clock::now();
|
|
startPts = pts;
|
|
haveClock = true;
|
|
} else {
|
|
const auto target =
|
|
startTime + std::chrono::duration_cast<clock::duration>(
|
|
std::chrono::duration<double>(pts - startPts));
|
|
std::this_thread::sleep_until(target);
|
|
}
|
|
masterClock.store(pts);
|
|
}
|
|
|
|
if (!running.load()) {
|
|
if (drm.fd >= 0) {
|
|
::close(drm.fd);
|
|
}
|
|
return;
|
|
}
|
|
if (isDrm) {
|
|
pushDrm(drm);
|
|
} else {
|
|
pushFrame(w, h, std::move(y), std::move(uv), colorspace, fullRange, pts);
|
|
}
|
|
};
|
|
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
std::vector<uint8_t> audioBuf;
|
|
auto process_audio = [&]() {
|
|
const int maxOut = swr_get_out_samples(swr, frame->nb_samples);
|
|
if (maxOut <= 0) {
|
|
return;
|
|
}
|
|
audioBuf.resize(static_cast<std::size_t>(maxOut) * kOutChannels * 2);
|
|
uint8_t* out[1] = {audioBuf.data()};
|
|
const int got =
|
|
swr_convert(swr, out, maxOut,
|
|
const_cast<const uint8_t**>(frame->extended_data),
|
|
frame->nb_samples);
|
|
if (got <= 0) {
|
|
return;
|
|
}
|
|
const std::size_t bytes = static_cast<std::size_t>(got) * kOutChannels * 2;
|
|
// Mute while actively scrubbing so audio writes don't block video previews
|
|
// and the sound doesn't stutter across seek positions.
|
|
if (std::chrono::duration<double>(clock::now() - lastSeekWall).count() < 0.25) {
|
|
return;
|
|
}
|
|
int err = 0;
|
|
pa_simple_write(pa, audioBuf.data(), bytes, &err);
|
|
|
|
int64_t ts = frame->best_effort_timestamp;
|
|
if (ts == AV_NOPTS_VALUE) {
|
|
ts = frame->pts;
|
|
}
|
|
if (ts != AV_NOPTS_VALUE) {
|
|
masterClock.store(ts * av_q2d(audioTimeBase));
|
|
}
|
|
};
|
|
#endif
|
|
|
|
auto drain = [&](AVCodecContext* ctx, bool video) {
|
|
while (avcodec_receive_frame(ctx, frame) == 0) {
|
|
if (video) {
|
|
process_video();
|
|
}
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
else {
|
|
process_audio();
|
|
}
|
|
#endif
|
|
if (!running.load()) {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
while (running.load()) {
|
|
double seekTo = seekTarget.exchange(-1.0);
|
|
// Throttle seek handling to ~8/s. Rapid scrubbing otherwise flushes the
|
|
// hardware decoder faster than it can emit a frame, wedging it so the
|
|
// picture freezes; deferred seeks are re-stored so the latest still wins.
|
|
if (seekTo >= 0.0) {
|
|
const double since =
|
|
std::chrono::duration<double>(clock::now() - lastSeekWall).count();
|
|
if (since < 0.12) {
|
|
double expected = -1.0;
|
|
seekTarget.compare_exchange_strong(expected, seekTo);
|
|
seekTo = -1.0;
|
|
}
|
|
}
|
|
if (seekTo >= 0.0) {
|
|
lastSeekWall = clock::now();
|
|
// Release every held dma-buf (queued frames + the renderer's live
|
|
// frame) before flushing; the V4L2 decoder cannot reclaim its capture
|
|
// buffers otherwise and wedges ("can't request capture buffers").
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex);
|
|
clearQueues(); // drop stale pre-seek frames
|
|
releaseReq.store(true);
|
|
releaseCv.wait_for(lock, std::chrono::milliseconds(150), [&] {
|
|
return !releaseReq.load() || !running.load();
|
|
});
|
|
}
|
|
const int64_t ts = static_cast<int64_t>(seekTo * AV_TIME_BASE);
|
|
avformat_seek_file(format, -1, INT64_MIN, ts, INT64_MAX, 0);
|
|
const bool reopened = reopenDecoder(); // fresh decoder avoids HW wedge
|
|
if (!reopened) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: video decoder reopen failed on seek");
|
|
}
|
|
PIXELVR_LOG_INFO("MediaEngine: seek %.2fs reopen=%d", seekTo, reopened);
|
|
drmDiag = 0; // re-log the first frames so seek recovery is visible
|
|
if (audioCodec != nullptr) {
|
|
avcodec_flush_buffers(audioCodec);
|
|
}
|
|
haveClock = false;
|
|
masterClock.store(seekTo);
|
|
playClock.reset(); // the next presented frame re-anchors the clock
|
|
eofFlushed = false;
|
|
eof.store(false);
|
|
}
|
|
if (paused.load()) {
|
|
haveClock = false; // re-establish the wall clock on resume
|
|
playClock.freeze(); // hold the clock so video doesn't advance
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
continue;
|
|
}
|
|
playClock.resume(); // no-op unless resuming from pause
|
|
const int readResult = av_read_frame(format, packet);
|
|
if (readResult < 0) {
|
|
if (!eofFlushed) {
|
|
avcodec_send_packet(codec, nullptr); // flush video
|
|
drain(codec, true);
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
if (audioEnabled) {
|
|
avcodec_send_packet(audioCodec, nullptr);
|
|
drain(audioCodec, false);
|
|
}
|
|
#endif
|
|
eofFlushed = true;
|
|
eof.store(true);
|
|
}
|
|
// Stay alive so a seek can resume instead of ending the thread.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|
continue;
|
|
}
|
|
|
|
if (packet->stream_index == videoStream) {
|
|
if (avcodec_send_packet(codec, packet) == 0) {
|
|
drain(codec, true);
|
|
}
|
|
}
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
else if (audioEnabled && packet->stream_index == audioStream) {
|
|
if (avcodec_send_packet(audioCodec, packet) == 0) {
|
|
drain(audioCodec, false);
|
|
}
|
|
}
|
|
#endif
|
|
av_packet_unref(packet);
|
|
}
|
|
|
|
av_packet_free(&packet);
|
|
av_frame_free(&frame);
|
|
}
|
|
|
|
MediaEngine::MediaEngine() : impl_(std::make_unique<Impl>()) {
|
|
avformat_network_init();
|
|
}
|
|
|
|
MediaEngine::~MediaEngine() {
|
|
close();
|
|
}
|
|
|
|
bool MediaEngine::open(const std::string& path) {
|
|
close();
|
|
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
if (path.rfind("smb://", 0) == 0) {
|
|
// libsmbclient is not thread-safe; serialize context setup against the
|
|
// browser's scans/downloads, which share Samba's global state.
|
|
SMBCCTX* ctx = nullptr;
|
|
SMBCFILE* file = nullptr;
|
|
{
|
|
std::lock_guard<std::mutex> smb(smbGlobalLock());
|
|
ctx = smbc_new_context();
|
|
if (ctx != nullptr) {
|
|
smbc_setFunctionAuthDataWithContext(ctx, &smb_auth);
|
|
if (smbc_init_context(ctx) == nullptr) {
|
|
smbc_free_context(ctx, 1);
|
|
ctx = nullptr;
|
|
}
|
|
}
|
|
smbc_open_fn openFn =
|
|
(ctx != nullptr) ? smbc_getFunctionOpen(ctx) : nullptr;
|
|
file = (openFn != nullptr) ? openFn(ctx, path.c_str(), O_RDONLY, 0)
|
|
: nullptr;
|
|
if (file == nullptr && ctx != nullptr) {
|
|
smbc_free_context(ctx, 1);
|
|
ctx = nullptr;
|
|
}
|
|
}
|
|
if (file == nullptr) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: cannot open SMB stream %s", path.c_str());
|
|
return false;
|
|
}
|
|
impl_->smbReader.start(ctx, file);
|
|
|
|
constexpr int kBuf = 1 << 16; // 64 KiB AVIO buffer (ring is separate)
|
|
auto* buffer = static_cast<unsigned char*>(av_malloc(kBuf));
|
|
impl_->avio = (buffer != nullptr)
|
|
? avio_alloc_context(buffer, kBuf, 0, &impl_->smbReader,
|
|
&smb_read, nullptr, &smb_seek)
|
|
: nullptr;
|
|
if (impl_->avio == nullptr) {
|
|
av_free(buffer);
|
|
close();
|
|
PIXELVR_LOG_ERROR("MediaEngine: SMB AVIO alloc failed");
|
|
return false;
|
|
}
|
|
impl_->format = avformat_alloc_context();
|
|
if (impl_->format == nullptr) {
|
|
close();
|
|
return false;
|
|
}
|
|
impl_->format->pb = impl_->avio;
|
|
impl_->format->flags |= AVFMT_FLAG_CUSTOM_IO;
|
|
if (avformat_open_input(&impl_->format, path.c_str(), nullptr, nullptr) < 0) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: cannot open SMB stream %s", path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
} else
|
|
#endif
|
|
if (avformat_open_input(&impl_->format, path.c_str(), nullptr, nullptr) < 0) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: cannot open %s", path.c_str());
|
|
return false;
|
|
}
|
|
if (avformat_find_stream_info(impl_->format, nullptr) < 0) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: no stream info in %s", path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
|
|
// Enumerate selectable video/audio tracks (with language metadata) so the UI
|
|
// can offer track + language selection.
|
|
impl_->videoStreamList.clear();
|
|
impl_->audioStreamList.clear();
|
|
for (unsigned si = 0; si < impl_->format->nb_streams; ++si) {
|
|
AVStream* s = impl_->format->streams[si];
|
|
const AVCodecParameters* cp = s->codecpar;
|
|
std::string lang;
|
|
if (AVDictionaryEntry* e =
|
|
av_dict_get(s->metadata, "language", nullptr, 0)) {
|
|
lang = e->value;
|
|
}
|
|
const AVCodec* dc = avcodec_find_decoder(cp->codec_id);
|
|
char buf[160];
|
|
if (cp->codec_type == AVMEDIA_TYPE_VIDEO) {
|
|
std::snprintf(buf, sizeof(buf), "%s %dx%d%s%s", dc ? dc->name : "?",
|
|
cp->width, cp->height, lang.empty() ? "" : " ",
|
|
lang.c_str());
|
|
impl_->videoStreamList.push_back({static_cast<int>(si), buf, lang});
|
|
} else if (cp->codec_type == AVMEDIA_TYPE_AUDIO) {
|
|
std::snprintf(buf, sizeof(buf), "%s %dch%s%s", dc ? dc->name : "?",
|
|
cp->ch_layout.nb_channels, lang.empty() ? "" : " ",
|
|
lang.c_str());
|
|
impl_->audioStreamList.push_back({static_cast<int>(si), buf, lang});
|
|
}
|
|
}
|
|
|
|
const int wantVideo =
|
|
(impl_->preferredVideoStream >= 0) ? impl_->preferredVideoStream : -1;
|
|
impl_->videoStream = av_find_best_stream(impl_->format, AVMEDIA_TYPE_VIDEO,
|
|
wantVideo, -1, nullptr, 0);
|
|
if (impl_->videoStream < 0) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: no video stream in %s", path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
|
|
AVStream* stream = impl_->format->streams[impl_->videoStream];
|
|
impl_->timeBase = stream->time_base;
|
|
impl_->frameRate = av_q2d(av_guess_frame_rate(impl_->format, stream, nullptr));
|
|
PIXELVR_LOG_INFO("MediaEngine: source frame rate %.3f fps", impl_->frameRate);
|
|
const AVCodecID codecId = stream->codecpar->codec_id;
|
|
|
|
// The Iris V4L2-M2M hardware path (H.264/HEVC/VP9) handles 8-bit here; route
|
|
// 10-bit+ to software (swscale downconverts to NV12) until dmabuf/P010 import
|
|
// lands. Prefer the hardware decoder by name; fall back to software so the
|
|
// same build also runs on a desktop for development.
|
|
const bool forceSoftware = std::getenv("PIXELVR_FORCE_SW") != nullptr;
|
|
const bool drmPrime = std::getenv("PIXELVR_DRM_PRIME") != nullptr;
|
|
const AVPixFmtDescriptor* srcDesc =
|
|
av_pix_fmt_desc_get(static_cast<AVPixelFormat>(stream->codecpar->format));
|
|
const bool highBitDepth = srcDesc != nullptr && srcDesc->comp[0].depth > 8;
|
|
// Hardware handles 8-bit for all codecs; 10-bit only for HEVC and only via the
|
|
// dmabuf/P010 path (drm_prime). Otherwise route high-bit-depth to software.
|
|
const bool hwEligible =
|
|
!highBitDepth || (drmPrime && codecId == AV_CODEC_ID_HEVC);
|
|
const char* hwName = !hwEligible ? nullptr
|
|
: (codecId == AV_CODEC_ID_H264) ? "h264_v4l2m2m"
|
|
: (codecId == AV_CODEC_ID_HEVC) ? "hevc_v4l2m2m"
|
|
: (codecId == AV_CODEC_ID_VP9) ? "vp9_v4l2m2m"
|
|
: nullptr;
|
|
const AVCodec* decoder = (hwName != nullptr && !forceSoftware)
|
|
? avcodec_find_decoder_by_name(hwName)
|
|
: nullptr;
|
|
bool hardware = decoder != nullptr;
|
|
if (decoder == nullptr) {
|
|
decoder = avcodec_find_decoder(codecId);
|
|
}
|
|
if (decoder == nullptr) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: no decoder for %s", path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
|
|
auto open_with = [&](const AVCodec* dec) -> bool {
|
|
if (impl_->codec != nullptr) {
|
|
avcodec_free_context(&impl_->codec);
|
|
}
|
|
impl_->codec = avcodec_alloc_context3(dec);
|
|
if (impl_->codec == nullptr ||
|
|
avcodec_parameters_to_context(impl_->codec, stream->codecpar) < 0) {
|
|
return false;
|
|
}
|
|
impl_->codec->pkt_timebase = stream->time_base;
|
|
impl_->codec->thread_count = 0; // Auto-detect and utilize all CPU cores
|
|
impl_->codec->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
|
|
impl_->videoDecoder = dec; // retained so the decode thread can reopen on seek
|
|
if (std::strstr(dec->name, "v4l2m2m") != nullptr) {
|
|
av_opt_set_int(impl_->codec->priv_data, "num_output_buffers", 4, 0);
|
|
}
|
|
impl_->decoderDrm = drmPrime && std::strstr(dec->name, "v4l2m2m") != nullptr;
|
|
if (impl_->decoderDrm) {
|
|
// Export decoder output as DMA-BUF (AV_PIX_FMT_DRM_PRIME) for zero-copy
|
|
// Vulkan import; requires the dmabuf-patched libavcodec. Bound the
|
|
// decoded-buffer memory: at 8K a frame is ~40-80 MiB, so a fixed large
|
|
// count exhausts the decoder's DMA pool and REQBUFS fails -> POLLERR.
|
|
const int64_t px = static_cast<int64_t>(stream->codecpar->width) *
|
|
stream->codecpar->height;
|
|
const int64_t frameBytes = highBitDepth ? px * 3 : px * 3 / 2; // P010/NV12
|
|
constexpr int64_t kBudget = 320ll << 20; // ~320 MiB capture pool
|
|
impl_->captureBuffers = static_cast<int>(std::clamp<int64_t>(
|
|
kBudget / std::max<int64_t>(frameBytes, 1), 4, 20));
|
|
av_opt_set_int(impl_->codec->priv_data, "num_capture_buffers",
|
|
impl_->captureBuffers, 0);
|
|
av_opt_set_int(impl_->codec->priv_data, "drm_prime", 1, 0);
|
|
impl_->codec->apply_cropping = 0;
|
|
// Leave buffers free for the decoder's DPB; the queue holds the rest.
|
|
// A deeper queue absorbs bursty decode output (B-frame reorder, SMB
|
|
// jitter) so low-framerate playback doesn't underrun and judder.
|
|
impl_->maxQueue =
|
|
static_cast<std::size_t>(std::clamp(impl_->captureBuffers - 5, 1, 14));
|
|
}
|
|
return avcodec_open2(impl_->codec, dec, nullptr) >= 0;
|
|
};
|
|
|
|
if (!open_with(decoder)) {
|
|
if (hardware) {
|
|
PIXELVR_LOG_WARN("MediaEngine: hardware decoder failed, trying software");
|
|
hardware = false;
|
|
decoder = avcodec_find_decoder(codecId);
|
|
if (decoder == nullptr || !open_with(decoder)) {
|
|
PIXELVR_LOG_ERROR("MediaEngine: failed to open decoder for %s",
|
|
path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
} else {
|
|
PIXELVR_LOG_ERROR("MediaEngine: failed to open decoder for %s",
|
|
path.c_str());
|
|
close();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
impl_->width = impl_->codec->width;
|
|
impl_->height = impl_->codec->height;
|
|
impl_->duration = (impl_->format->duration > 0)
|
|
? impl_->format->duration / static_cast<double>(AV_TIME_BASE)
|
|
: 0.0;
|
|
|
|
PIXELVR_LOG_INFO("MediaEngine: %s %dx%d %.1fs codec=%s (%s)", path.c_str(),
|
|
impl_->width, impl_->height, impl_->duration, decoder->name,
|
|
hardware ? "hardware" : "software");
|
|
impl_->codecName =
|
|
std::string(decoder->name) + (hardware ? " (hw)" : " (sw)");
|
|
impl_->decodedCount.store(0);
|
|
|
|
#ifdef PIXELVR_HAVE_PULSE
|
|
const AVCodec* adec = nullptr;
|
|
int wantAudio = impl_->preferredAudioStream;
|
|
if (wantAudio < 0 && !impl_->preferredLanguage.empty()) {
|
|
for (const auto& si : impl_->audioStreamList) {
|
|
if (si.language == impl_->preferredLanguage) {
|
|
wantAudio = si.index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
impl_->audioStream = av_find_best_stream(impl_->format, AVMEDIA_TYPE_AUDIO,
|
|
wantAudio, -1, &adec, 0);
|
|
if (impl_->audioStream >= 0 && adec != nullptr) {
|
|
AVStream* as = impl_->format->streams[impl_->audioStream];
|
|
impl_->audioTimeBase = as->time_base;
|
|
impl_->audioCodec = avcodec_alloc_context3(adec);
|
|
if (impl_->audioCodec != nullptr &&
|
|
avcodec_parameters_to_context(impl_->audioCodec, as->codecpar) >= 0 &&
|
|
avcodec_open2(impl_->audioCodec, adec, nullptr) >= 0) {
|
|
AVChannelLayout outLayout;
|
|
av_channel_layout_default(&outLayout, MediaEngine::Impl::kOutChannels);
|
|
if (swr_alloc_set_opts2(&impl_->swr, &outLayout, AV_SAMPLE_FMT_S16,
|
|
MediaEngine::Impl::kOutSampleRate,
|
|
&impl_->audioCodec->ch_layout,
|
|
impl_->audioCodec->sample_fmt,
|
|
impl_->audioCodec->sample_rate, 0, nullptr) == 0 &&
|
|
swr_init(impl_->swr) >= 0) {
|
|
pa_sample_spec spec;
|
|
spec.format = PA_SAMPLE_S16LE;
|
|
spec.rate = MediaEngine::Impl::kOutSampleRate;
|
|
spec.channels = MediaEngine::Impl::kOutChannels;
|
|
impl_->pa =
|
|
pa_simple_new(nullptr, "PixelVRPlayer", PA_STREAM_PLAYBACK,
|
|
nullptr, "video", &spec, nullptr, nullptr, nullptr);
|
|
impl_->audioEnabled = (impl_->pa != nullptr);
|
|
}
|
|
av_channel_layout_uninit(&outLayout);
|
|
}
|
|
if (impl_->audioEnabled) {
|
|
PIXELVR_LOG_INFO("MediaEngine: audio %d Hz stereo",
|
|
MediaEngine::Impl::kOutSampleRate);
|
|
} else {
|
|
PIXELVR_LOG_WARN("MediaEngine: audio unavailable, video-only playback");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
impl_->eof.store(false);
|
|
impl_->running.store(true);
|
|
impl_->thread = std::thread([this] { impl_->decodeLoop(); });
|
|
return true;
|
|
}
|
|
|
|
void MediaEngine::close() {
|
|
if (!impl_) {
|
|
return;
|
|
}
|
|
impl_->running.store(false);
|
|
{
|
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
impl_->releaseReq.store(false); // wake a decode thread in the seek handshake
|
|
impl_->releaseCv.notify_all();
|
|
impl_->queueCv.notify_all(); // wake a back-pressured decoder to exit
|
|
}
|
|
#ifdef PIXELVR_HAVE_SMB
|
|
impl_->smbReader.abort(); // unblock a decode thread stalled in smb_read
|
|
#endif
|
|
if (impl_->thread.joinable()) {
|
|
impl_->thread.join();
|
|
}
|
|
impl_->teardown();
|
|
impl_->videoStream = -1;
|
|
impl_->width = impl_->height = 0;
|
|
impl_->duration = 0.0;
|
|
impl_->eof.store(false);
|
|
}
|
|
|
|
int MediaEngine::width() const { return impl_->width; }
|
|
int MediaEngine::height() const { return impl_->height; }
|
|
std::string MediaEngine::codecName() const { return impl_->codecName; }
|
|
uint64_t MediaEngine::decodedFrames() const {
|
|
return impl_->decodedCount.load(std::memory_order_relaxed);
|
|
}
|
|
double MediaEngine::duration() const { return impl_->duration; }
|
|
double MediaEngine::frameRate() const { return impl_ ? impl_->frameRate : 0.0; }
|
|
bool MediaEngine::eof() const { return impl_->eof.load(); }
|
|
|
|
void MediaEngine::setPaused(bool paused) {
|
|
if (impl_) impl_->paused.store(paused);
|
|
}
|
|
|
|
void MediaEngine::togglePause() {
|
|
if (impl_) impl_->paused.store(!impl_->paused.load());
|
|
}
|
|
|
|
bool MediaEngine::paused() const {
|
|
return impl_ && impl_->paused.load();
|
|
}
|
|
|
|
void MediaEngine::seek(double seconds) {
|
|
if (!impl_) return;
|
|
if (impl_->duration > 0.0 && seconds > impl_->duration) {
|
|
seconds = impl_->duration;
|
|
}
|
|
if (seconds < 0.0) seconds = 0.0;
|
|
{
|
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
impl_->seekTarget.store(seconds);
|
|
}
|
|
impl_->queueCv.notify_all(); // wake a back-pressured decoder to handle the seek
|
|
}
|
|
|
|
void MediaEngine::seekRelative(double delta) {
|
|
seek(position() + delta);
|
|
}
|
|
|
|
double MediaEngine::position() const {
|
|
return impl_ ? impl_->masterClock.load() : 0.0;
|
|
}
|
|
|
|
std::vector<MediaEngine::StreamInfo> MediaEngine::videoStreams() const {
|
|
return impl_ ? impl_->videoStreamList : std::vector<StreamInfo>{};
|
|
}
|
|
|
|
std::vector<MediaEngine::StreamInfo> MediaEngine::audioStreams() const {
|
|
return impl_ ? impl_->audioStreamList : std::vector<StreamInfo>{};
|
|
}
|
|
|
|
int MediaEngine::currentVideoStream() const {
|
|
return impl_ ? impl_->videoStream : -1;
|
|
}
|
|
|
|
int MediaEngine::currentAudioStream() const {
|
|
return impl_ ? impl_->audioStream : -1;
|
|
}
|
|
|
|
void MediaEngine::setPreferredVideoStream(int containerIndex) {
|
|
if (impl_) impl_->preferredVideoStream = containerIndex;
|
|
}
|
|
|
|
void MediaEngine::setPreferredAudioStream(int containerIndex) {
|
|
if (impl_) impl_->preferredAudioStream = containerIndex;
|
|
}
|
|
|
|
void MediaEngine::setPreferredAudioLanguage(const std::string& isoLang) {
|
|
if (impl_) impl_->preferredLanguage = isoLang;
|
|
}
|
|
|
|
bool MediaEngine::needsFrameRelease() const {
|
|
return impl_ && impl_->releaseReq.load();
|
|
}
|
|
|
|
void MediaEngine::confirmFrameRelease() {
|
|
if (!impl_) return;
|
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
impl_->releaseReq.store(false);
|
|
impl_->releaseCv.notify_all();
|
|
}
|
|
|
|
// Chooses the queued frame to display this vsync: the newest whose PTS is due
|
|
// against the master clock. Earlier (late) frames are dropped; later (early)
|
|
// frames are held for a future vsync. Returns the index to serve, or npos when
|
|
// every frame is still in the future and the screen has already been primed.
|
|
static std::size_t selectDueIndex(double clock, bool primed,
|
|
const std::function<double(std::size_t)>& ptsAt,
|
|
std::size_t count) {
|
|
std::size_t due = 0;
|
|
bool haveDue = false;
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
if (ptsAt(i) <= clock) {
|
|
due = i;
|
|
haveDue = true;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (!haveDue) {
|
|
return primed ? static_cast<std::size_t>(-1) : 0; // hold, or prime
|
|
}
|
|
return due;
|
|
}
|
|
|
|
bool MediaEngine::latestFrame(VideoFrame& out) {
|
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
auto& q = impl_->swQueue;
|
|
if (q.empty()) {
|
|
return false;
|
|
}
|
|
double clock = impl_->currentClock();
|
|
// Safety net: re-anchor if the clock has fallen behind the queued frames,
|
|
// so the picture can never freeze "early forever" while decoding continues.
|
|
if (impl_->primed && q.front().pts > clock + 0.25) {
|
|
impl_->playClock.start(q.front().pts);
|
|
clock = q.front().pts;
|
|
}
|
|
const std::size_t due = selectDueIndex(
|
|
clock, impl_->primed,
|
|
[&](std::size_t i) { return q[i].pts; }, q.size());
|
|
if (due == static_cast<std::size_t>(-1)) {
|
|
return false; // all queued frames are still in the future
|
|
}
|
|
for (std::size_t i = 0; i < due; ++i) {
|
|
q.pop_front(); // drop superseded (late) frames
|
|
}
|
|
const bool firstFrame = !impl_->primed;
|
|
out = std::move(q.front());
|
|
q.pop_front();
|
|
impl_->primed = true;
|
|
if (firstFrame) impl_->playClock.start(out.pts); // anchor to the first frame
|
|
impl_->queueCv.notify_one(); // slot freed: wake a back-pressured decoder
|
|
return true;
|
|
}
|
|
|
|
bool MediaEngine::drmActive() const {
|
|
return impl_->usingDrm.load();
|
|
}
|
|
|
|
bool MediaEngine::latestDrmFrame(DrmVideoFrame& out) {
|
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
auto& q = impl_->drmQueue;
|
|
if (q.empty()) {
|
|
return false;
|
|
}
|
|
double clock = impl_->currentClock();
|
|
// Safety net: re-anchor if the clock has fallen behind the queued frames,
|
|
// so the picture can never freeze "early forever" while decoding continues.
|
|
if (impl_->primed && q.front().pts > clock + 0.25) {
|
|
PIXELVR_LOG_INFO("MediaEngine: clock resync %.2f -> %.2f", clock,
|
|
q.front().pts);
|
|
impl_->playClock.start(q.front().pts);
|
|
clock = q.front().pts;
|
|
}
|
|
const std::size_t due = selectDueIndex(
|
|
clock, impl_->primed,
|
|
[&](std::size_t i) { return q[i].pts; }, q.size());
|
|
if (due == static_cast<std::size_t>(-1)) {
|
|
return false; // all queued frames are still in the future
|
|
}
|
|
for (std::size_t i = 0; i < due; ++i) {
|
|
if (q.front().fd >= 0) ::close(q.front().fd); // release dropped dma-buf
|
|
q.pop_front();
|
|
}
|
|
if (out.fd >= 0) {
|
|
::close(out.fd); // release the caller's previous unconsumed handle
|
|
}
|
|
const bool firstFrame = !impl_->primed;
|
|
out = std::move(q.front()); // transfers fd + keepAlive ownership to caller
|
|
q.pop_front();
|
|
impl_->primed = true;
|
|
if (firstFrame) {
|
|
impl_->playClock.start(out.pts); // anchor to the first frame
|
|
PIXELVR_LOG_INFO("MediaEngine: prime pts=%.2f clock=%.2f", out.pts, clock);
|
|
}
|
|
impl_->queueCv.notify_one(); // slot freed: wake a back-pressured decoder
|
|
return true;
|
|
}
|
|
|
|
} // namespace pixelvr
|