Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/plugins/score-plugin-gfx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ set(HDRS
Gfx/Graph/Utils.hpp
Gfx/Graph/VideoNode.hpp
Gfx/Graph/VideoNodeRenderer.hpp
Gfx/Graph/SyncVideoNode.hpp
Gfx/Graph/Window.hpp
Gfx/Graph/decoders/GPUVideoDecoder.hpp
Gfx/Graph/decoders/HAP.hpp
Expand Down Expand Up @@ -256,6 +257,7 @@ set(SRCS
Gfx/Graph/Utils.cpp
Gfx/Graph/VideoNode.cpp
Gfx/Graph/VideoNodeRenderer.cpp
Gfx/Graph/SyncVideoNode.cpp
Gfx/Graph/Window.cpp

Gfx/GfxApplicationPlugin.cpp
Expand Down
101 changes: 101 additions & 0 deletions src/plugins/score-plugin-gfx/Gfx/Graph/SyncVideoNode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <Gfx/Graph/SyncVideoNode.hpp>
#include <Gfx/Graph/VideoNodeRenderer.hpp>
#include <Video/SyncVideoDecoder.hpp>

#include <score/tools/Debug.hpp>

#include <ossia/detail/flicks.hpp>

namespace score::gfx
{

SyncVideoFrameReader::SyncVideoFrameReader() { }

SyncVideoFrameReader::~SyncVideoFrameReader()
{
// Release any frames we're holding
releaseAllFrames();
}

void SyncVideoFrameReader::readNextFrame(double currentTime)
{
auto& decoder = *m_decoder;

// Convert current time (seconds) to flicks
int64_t target_flicks = currentTime * ossia::flicks_per_second<double>;

// Avoid redundant decode requests for the same time
if(target_flicks == m_lastRequestedFlicks && m_currentFrame)
{
return;
}

m_lastRequestedFlicks = target_flicks;

// Request frame at exact time from sync decoder
if(auto frame = decoder.decode_frame_at(target_flicks))
{
updateCurrentFrame(frame);
}

releaseFramesToFree();
}

SyncVideoNode::SyncVideoNode(
std::shared_ptr<Video::VideoInterface> dec, std::optional<double> nativeTempo)
: m_nativeTempo{nativeTempo}
{
this->reader.m_decoder = std::move(dec);
output.push_back(new Port{this, {}, Types::Image, {}});
}

SyncVideoNode::~SyncVideoNode() { }

score::gfx::NodeRenderer* SyncVideoNode::createRenderer(RenderList& r) const noexcept
{
// Reuse VideoNodeRenderer - it just reads from VideoFrameShare
return new VideoNodeRenderer{
*this, const_cast<VideoFrameShare&>(static_cast<const VideoFrameShare&>(reader))};
}

void SyncVideoNode::process(Message&& msg)
{
m_lastToken = msg.token;
m_timer.start();
}

void SyncVideoNode::update()
{
if(m_pause)
{
m_timer.restart();
return;
}

ProcessNode::process(m_lastToken);
auto elapsed = m_timer.nsecsElapsed() / 1e9;
this->standardUBO.time += elapsed;

// Calculate current playback time with tempo adjustment
double tempoRatio = 1.;
if(m_nativeTempo)
{
if(*m_nativeTempo <= 0.)
{
return;
}
tempoRatio = (*m_nativeTempo) / 120.;
}

double currentTime = this->standardUBO.time * tempoRatio;

// Direct sync decode at the current time
reader.readNextFrame(currentTime);
}

void SyncVideoNode::pause(bool b)
{
m_pause = b;
}

}
69 changes: 69 additions & 0 deletions src/plugins/score-plugin-gfx/Gfx/Graph/SyncVideoNode.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once

#include <Gfx/Graph/VideoNode.hpp>

namespace Video
{
class SyncVideoDecoder;
}

namespace score::gfx
{

/**
* @brief Frame reader for synchronous video decoding
*
* Unlike VideoFrameReader which manages async queue polling and drift correction,
* SyncVideoFrameReader directly requests frames at specific times from the decoder.
* This provides frame-accurate positioning with zero latency.
*/
struct SyncVideoFrameReader : VideoFrameShare
{
SyncVideoFrameReader();
~SyncVideoFrameReader();

void readNextFrame(double currentTime);

private:
int64_t m_lastRequestedFlicks{-1};
};

/**
* @brief Video node for synchronous (frame-accurate) video playback
*
* This node uses SyncVideoDecoder to decode frames on-demand, providing:
* - Zero-latency seeking
* - Frame-accurate synchronization
* - No buffer delay
*
* Use this when precise timing is critical (e.g., syncing with audio,
* external timecode, or interactive control).
*/
class SCORE_PLUGIN_GFX_EXPORT SyncVideoNode : public VideoNodeBase
{
public:
SyncVideoNode(
std::shared_ptr<Video::VideoInterface> dec, std::optional<double> nativeTempo);

virtual ~SyncVideoNode();

score::gfx::NodeRenderer* createRenderer(RenderList& r) const noexcept override;

void process(Message&& msg) override;
void update() override;

SyncVideoFrameReader reader;

void pause(bool);

private:
friend SyncVideoFrameReader;
friend VideoNodeRenderer;

std::optional<double> m_nativeTempo;
Timings m_lastToken{};
QElapsedTimer m_timer;
std::atomic_bool m_pause{};
};

}
Loading
Loading