ladybird/Libraries/LibMedia/Sinks/DisplayingVideoSink.cpp
Zaggy1024 ae04222247 LibMedia: Don't display video frames that are late
Frames are considered late if the time is ahead by half their duration.
The fudging is necessary because Matroska (and perhaps other formats)
store their frame durations in different time units than their
timestamps.

Skipping these should make it clear when decoding is running behind,
instead of displaying the video in slow motion while audio runs at a
normal rate.

To give an accurate counting for video playback quality when it is
implemented, we'll most likely want to count all pulled frames in an
update as dropped if the last frame is dropped. Otherwise the frame
drop count will only increase at the display rate when decoding is
continually running behind.
2026-06-11 21:49:55 +02:00

199 lines
6.5 KiB
C++

/*
* Copyright (c) 2025, Gregory Bertilson <gregory@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/EventLoop.h>
#include <LibMedia/Demuxer.h>
#include <LibMedia/MediaTimeProvider.h>
#include <LibMedia/PipelineStatus.h>
#include <LibMedia/Producers/VideoProducer.h>
#include <LibMedia/VideoFrame.h>
#include "DisplayingVideoSink.h"
namespace Media {
ErrorOr<NonnullRefPtr<DisplayingVideoSink>> DisplayingVideoSink::try_create(NonnullRefPtr<MediaTimeProvider> const& time_provider, PipelineStateChangeHandler on_state_changed)
{
return TRY(try_make_ref_counted<DisplayingVideoSink>(time_provider, move(on_state_changed)));
}
DisplayingVideoSink::DisplayingVideoSink(NonnullRefPtr<MediaTimeProvider> const& time_provider, PipelineStateChangeHandler on_state_changed)
: m_time_provider(time_provider)
, m_on_state_changed(move(on_state_changed))
{
}
DisplayingVideoSink::~DisplayingVideoSink()
{
if (m_input != nullptr)
m_input->set_wake_handler(nullptr);
}
void DisplayingVideoSink::set_time_provider(NonnullRefPtr<MediaTimeProvider> const& provider)
{
m_time_provider = provider;
}
void DisplayingVideoSink::consume_moved_position_signals(PipelineStatus& status)
{
while ((status = m_input->status()) == PipelineStatus::MovedPosition) {
m_input->pull(m_next_frame);
VERIFY(m_next_frame == nullptr);
m_seek_status = SeekStatus::FrameInvalidated;
}
VERIFY(status != PipelineStatus::MovedPosition);
}
ErrorOr<void> DisplayingVideoSink::connect_input(NonnullRefPtr<VideoProducer> const& input)
{
VERIFY(m_input == nullptr);
m_input = input;
input->set_wake_handler([this, input] {
auto status = PipelineStatus::Pending;
consume_moved_position_signals(status);
if (!resolves_seek(status))
return;
if (m_current_frame != nullptr && m_seek_status == SeekStatus::None)
status = PipelineStatus::HaveData;
dispatch_state_if_changed(status);
});
input->seek(m_time_provider->current_time());
input->start();
return {};
}
void DisplayingVideoSink::disconnect_input(NonnullRefPtr<VideoProducer> const& input)
{
VERIFY(m_input == input);
m_input->set_wake_handler(nullptr);
m_input = nullptr;
}
static AK::Duration conservative_frame_end(VideoFrame& frame)
{
return frame.timestamp() + frame.duration().scaled_by(3, 2);
}
void DisplayingVideoSink::seek(AK::Duration timestamp)
{
m_seek_id++;
m_last_dispatched_status = PipelineStatus::Pending;
auto can_resolve_seek_within_cached_frames = [&] {
if (m_seek_status != SeekStatus::None)
return false;
if (m_cached_frames_are_discontinuous)
return false;
auto available_start = AK::Duration::max();
auto available_end = AK::Duration::min();
auto include_frame = [&](RefPtr<VideoFrame> const& frame) {
if (frame == nullptr)
return;
available_start = min(available_start, frame->timestamp());
available_end = max(available_end, conservative_frame_end(*frame));
};
include_frame(m_current_frame);
include_frame(m_next_frame);
return timestamp >= available_start && timestamp < available_end;
}();
if (can_resolve_seek_within_cached_frames) {
Core::deferred_invoke([self = NonnullRefPtr(*this), seek_id = m_seek_id] {
if (self->m_seek_id != seek_id)
return;
self->dispatch_state_if_changed(PipelineStatus::HaveData);
VERIFY(self->m_seek_status == SeekStatus::None);
});
return;
}
if (m_input != nullptr)
m_input->seek(timestamp);
if (m_seek_status == SeekStatus::None)
m_seek_status = SeekStatus::InProgress;
}
void DisplayingVideoSink::dispatch_state_if_changed(PipelineStatus status)
{
if (status == m_last_dispatched_status)
return;
m_last_dispatched_status = status;
if (m_on_state_changed)
m_on_state_changed(status);
}
DisplayingVideoSinkUpdateResult DisplayingVideoSink::update()
{
if (m_input == nullptr)
return DisplayingVideoSinkUpdateResult::NoChange;
auto current_time = m_time_provider->current_time();
auto result = DisplayingVideoSinkUpdateResult::NoChange;
auto last_status = PipelineStatus::Pending;
while (true) {
if (m_next_frame == nullptr || m_seek_status != SeekStatus::None)
consume_moved_position_signals(last_status);
if (m_next_frame == nullptr) {
if (last_status != PipelineStatus::HaveData)
break;
m_input->pull(m_next_frame);
VERIFY(m_next_frame != nullptr);
if (m_seek_status == SeekStatus::FrameInvalidated)
m_current_frame.clear();
}
if (resolves_seek(last_status))
m_seek_status = SeekStatus::None;
if (m_seek_status != SeekStatus::None)
break;
if (m_next_frame->timestamp() > current_time)
break;
if (current_time > conservative_frame_end(*m_next_frame)) {
consume_moved_position_signals(last_status);
if (m_next_frame == nullptr)
continue;
if (!is_terminal(last_status)) {
if (last_status == PipelineStatus::HaveData) {
m_next_frame.clear();
m_cached_frames_are_discontinuous = true;
continue;
}
break;
}
}
m_current_frame = m_next_frame.release_nonnull();
m_cached_frames_are_discontinuous = false;
result = DisplayingVideoSinkUpdateResult::NewFrameAvailable;
}
if (m_current_frame != nullptr && m_seek_status == SeekStatus::None) {
AK::Duration current_frame_end;
if (is_terminal(last_status))
current_frame_end = m_current_frame->timestamp() + m_current_frame->duration();
else
current_frame_end = conservative_frame_end(*m_current_frame);
if (current_time <= current_frame_end)
last_status = PipelineStatus::HaveData;
}
// Dispatch the new state with a deferred invoke to avoid reentrancy. This prevents a seek from resolving while
// an update is being processed.
Core::deferred_invoke([self = NonnullRefPtr(*this), last_status, seek_id = m_seek_id] {
if (seek_id != self->m_seek_id)
return;
self->dispatch_state_if_changed(last_status);
});
return result;
}
RefPtr<VideoFrame> DisplayingVideoSink::current_frame()
{
return m_current_frame;
}
}