Files
oak-editor/app/node/output/viewer/viewer.cpp
T
Mike-Solar 3a3ead6fc0 app: route all FFmpeg access through the ffmpeg_bridge C API
Replace every direct FFmpeg call in the editor and the render worker
with the pure C ffmpeg_bridge API, wrapped in thin C++ adapters that
preserve the original interfaces:

- avframeptr.h: olive::AVFrame adapter around FBFrame handles
- ffmpegutils: format conversion helpers on FB_* constants; the int
  overload is renamed GetCompatibleBridgePixelFormat to avoid a silent
  overload-resolution trap with the PixelFormat enum
- ffmpegdecoder/ffmpegencoder: rewritten as handle-based adapters over
  FBDecoder/FBProbe/FBEncoder/FBScaler/FBResampler
- audioprocessor: FBAudioGraph push/pull adapter
- pluginrenderer/OliveClip: sws/pixdesc usage converted to FBScaler and
  fb_pix_fmt_* queries
- AudioParams/channel layouts are plain uint64_t masks everywhere

Build integration: the root project no longer links FFMPEG directly;
only ffmpeg_bridge does. Binaries resolve the bridge library at runtime
via @loader_path inside the macOS app bundle (copied there post-build)
and via $ORIGIN/../ffmpeg_bridge/bin on Linux; on Windows the DLL is
installed next to the executables, so packages on all three platforms
ship the bridge library.

ffmpeg_bridge gains the extra API the adapters need:
fb_frame_make_writable, fb_decoder_get_format_duration,
fb_resampler_convert_frame, FB_PIX_FMT_YUV440P, SRT validation in
fb_probe_read_subtitle_stream, packed/planar fixes in
fb_encoder_write_audio, and a component-size fix in
fb_pix_fmt_component_size.
2026-07-15 23:00:06 +08:00

669 lines
17 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "viewer.h"
#include "config/config.h"
#include "core.h"
#include "node/traverser.h"
namespace olive
{
const QString ViewerOutput::kVideoParamsInput =
QStringLiteral("video_param_in");
const QString ViewerOutput::kAudioParamsInput =
QStringLiteral("audio_param_in");
const QString ViewerOutput::kSubtitleParamsInput =
QStringLiteral("subtitle_param_in");
const QString ViewerOutput::kTextureInput = QStringLiteral("tex_in");
const QString ViewerOutput::kSamplesInput = QStringLiteral("samples_in");
const SampleFormat ViewerOutput::kDefaultSampleFormat = SampleFormat::F32P;
#define super Node
ViewerOutput::ViewerOutput(bool create_buffer_inputs,
bool create_default_streams)
: last_length_(0)
, video_length_(0)
, audio_length_(0)
, autocache_input_video_(true)
, autocache_input_audio_(false)
, waveform_requests_enabled_(false)
{
AddInput(kVideoParamsInput, NodeValue::kVideoParams,
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
kInputFlagArray | kInputFlagHidden));
AddInput(kAudioParamsInput, NodeValue::kAudioParams,
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
kInputFlagArray | kInputFlagHidden));
AddInput(kSubtitleParamsInput, NodeValue::kSubtitleParams,
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
kInputFlagArray | kInputFlagHidden));
if (create_buffer_inputs) {
AddInput(kTextureInput, NodeValue::kTexture,
InputFlags(kInputFlagNotKeyframable));
AddInput(kSamplesInput, NodeValue::kSamples,
InputFlags(kInputFlagNotKeyframable));
}
if (create_default_streams) {
AddStream(Track::kVideo, QVariant());
AddStream(Track::kAudio, QVariant());
set_default_parameters();
}
SetFlag(kDontShowInParamView);
workarea_ = new TimelineWorkArea(this);
markers_ = new TimelineMarkerList(this);
}
QString ViewerOutput::Name() const
{
return tr("Viewer");
}
QString ViewerOutput::id() const
{
return QStringLiteral("org.olivevideoeditor.Olive.vieweroutput");
}
QVector<Node::CategoryID> ViewerOutput::Category() const
{
return { kCategoryOutput };
}
QString ViewerOutput::Description() const
{
return tr("Interface between a Viewer panel and the node system.");
}
QVariant ViewerOutput::data(const DataType &d) const
{
switch (d) {
case DURATION: {
rational using_timebase;
Timecode::Display using_display =
Core::instance()->GetTimecodeDisplay();
// Get first enabled streams
VideoParams video = GetFirstEnabledVideoStream();
AudioParams audio = GetFirstEnabledAudioStream();
SubtitleParams sub = GetFirstEnabledSubtitleStream();
if (video.is_valid() &&
video.video_type() != VideoParams::kVideoTypeStill) {
// Prioritize video
using_timebase = video.frame_rate_as_time_base();
} else if (audio.is_valid()) {
// Use audio as a backup
// If we're showing in a timecode, we prefer showing audio in seconds instead
if (using_display == Timecode::kTimecodeDropFrame ||
using_display == Timecode::kTimecodeNonDropFrame) {
using_display = Timecode::kTimecodeSeconds;
}
using_timebase = audio.sample_rate_as_time_base();
} else if (sub.is_valid()) {
using_timebase =
OLIVE_CONFIG("DefaultSequenceFrameRate").value<rational>();
}
if (!using_timebase.isNull()) {
// Return time transformed to timecode
return QString::fromStdString(Timecode::time_to_timecode(
GetLength(), using_timebase, using_display));
}
break;
}
case FREQUENCY_RATE: {
VideoParams video_stream;
if (HasEnabledVideoStreams() &&
(video_stream = GetFirstEnabledVideoStream()).video_type() !=
VideoParams::kVideoTypeStill) {
// This is a video editor, prioritize video streams
return tr("%1 FPS").arg(video_stream.frame_rate().toDouble());
} else if (HasEnabledAudioStreams()) {
// No video streams, return audio
AudioParams audio_stream = GetFirstEnabledAudioStream();
return tr("%1 Hz").arg(audio_stream.sample_rate());
}
break;
}
default:
break;
}
return super::data(d);
}
bool ViewerOutput::HasEnabledVideoStreams() const
{
return GetFirstEnabledVideoStream().is_valid();
}
bool ViewerOutput::HasEnabledAudioStreams() const
{
return GetFirstEnabledAudioStream().is_valid();
}
bool ViewerOutput::HasEnabledSubtitleStreams() const
{
return GetFirstEnabledSubtitleStream().is_valid();
}
VideoParams ViewerOutput::GetFirstEnabledVideoStream() const
{
int sz = GetVideoStreamCount();
for (int i = 0; i < sz; i++) {
VideoParams vp = GetVideoParams(i);
if (vp.enabled()) {
return vp;
}
}
return VideoParams();
}
AudioParams ViewerOutput::GetFirstEnabledAudioStream() const
{
int sz = GetAudioStreamCount();
for (int i = 0; i < sz; i++) {
AudioParams ap = GetAudioParams(i);
if (ap.enabled()) {
return ap;
}
}
return AudioParams();
}
SubtitleParams ViewerOutput::GetFirstEnabledSubtitleStream() const
{
int sz = GetSubtitleStreamCount();
for (int i = 0; i < sz; i++) {
SubtitleParams sp = GetSubtitleParams(i);
if (sp.enabled()) {
return sp;
}
}
return SubtitleParams();
}
void ViewerOutput::set_default_parameters()
{
int width = OLIVE_CONFIG("DefaultSequenceWidth").toInt();
int height = OLIVE_CONFIG("DefaultSequenceHeight").toInt();
SetVideoParams(VideoParams(
width, height,
OLIVE_CONFIG("DefaultSequenceFrameRate").value<rational>(),
static_cast<PixelFormat::Format>(
OLIVE_CONFIG("OfflinePixelFormat").toInt()),
VideoParams::kInternalChannelCount,
OLIVE_CONFIG("DefaultSequencePixelAspect").value<rational>(),
OLIVE_CONFIG("DefaultSequenceInterlacing")
.value<VideoParams::Interlacing>(),
1));
SetAudioParams(
AudioParams(OLIVE_CONFIG("DefaultSequenceAudioFrequency").toInt(),
OLIVE_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
kDefaultSampleFormat));
}
void ViewerOutput::InvalidateCache(const TimeRange &range, const QString &from,
int element, InvalidateCacheOptions options)
{
Q_UNUSED(element)
if (Node *connected = GetConnectedOutput(from, element)) {
if (from == kTextureInput) {
//connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly);
if (autocache_input_video_) {
TimeRange max_range = InputTimeAdjustment(
from, element, TimeRange(0, GetVideoLength()), false);
connected->video_frame_cache()->Request(
this, range.Intersected(max_range));
}
} else if (from == kSamplesInput) {
TimeRange max_range = InputTimeAdjustment(
from, element, TimeRange(0, GetAudioLength()), false);
if (waveform_requests_enabled_) {
connected->waveform_cache()->Request(
this, range.Intersected(max_range));
}
if (autocache_input_audio_) {
connected->audio_playback_cache()->Request(
this, range.Intersected(max_range));
}
}
}
VerifyLength();
super::InvalidateCache(range, from, element, options);
}
QVector<Track::Reference> ViewerOutput::GetEnabledStreamsAsReferences() const
{
QVector<Track::Reference> refs;
{
int vp_sz = GetVideoStreamCount();
for (int i = 0; i < vp_sz; i++) {
if (GetVideoParams(i).enabled()) {
refs.append(Track::Reference(Track::kVideo, i));
}
}
}
{
int ap_sz = GetAudioStreamCount();
for (int i = 0; i < ap_sz; i++) {
if (GetAudioParams(i).enabled()) {
refs.append(Track::Reference(Track::kAudio, i));
}
}
}
{
int sp_sz = GetSubtitleStreamCount();
for (int i = 0; i < sp_sz; i++) {
if (GetSubtitleParams(i).enabled()) {
refs.append(Track::Reference(Track::kSubtitle, i));
}
}
}
return refs;
}
void ViewerOutput::Retranslate()
{
super::Retranslate();
SetInputName(kVideoParamsInput, tr("Video Parameters"));
SetInputName(kAudioParamsInput, tr("Audio Parameters"));
SetInputName(kSubtitleParamsInput, tr("Subtitle Parameters"));
if (HasInputWithID(kTextureInput)) {
SetInputName(kTextureInput, tr("Texture"));
}
if (HasInputWithID(kSamplesInput)) {
SetInputName(kSamplesInput, tr("Samples"));
}
}
void ViewerOutput::VerifyLength()
{
video_length_ = VerifyLengthInternal(Track::kVideo);
audio_length_ = VerifyLengthInternal(Track::kAudio);
rational subtitle_length = VerifyLengthInternal(Track::kSubtitle);
rational real_length =
qMax(subtitle_length, qMax(video_length_, audio_length_));
if (real_length != last_length_) {
last_length_ = real_length;
emit LengthChanged(last_length_);
}
}
void ViewerOutput::SetPlayhead(const rational &t)
{
playhead_ = t;
emit PlayheadChanged(t);
}
void ViewerOutput::InputConnectedEvent(const QString &input, int element,
Node *output)
{
if (input == kTextureInput) {
emit TextureInputChanged();
} else if (input == kSamplesInput) {
connect(output->waveform_cache(), &AudioWaveformCache::Validated, this,
&ViewerOutput::ConnectedWaveformChanged);
}
super::InputConnectedEvent(input, element, output);
}
void ViewerOutput::InputDisconnectedEvent(const QString &input, int element,
Node *output)
{
if (input == kTextureInput) {
emit TextureInputChanged();
} else if (input == kSamplesInput) {
disconnect(output->waveform_cache(), &AudioWaveformCache::Validated,
this, &ViewerOutput::ConnectedWaveformChanged);
}
super::InputDisconnectedEvent(input, element, output);
}
rational ViewerOutput::VerifyLengthInternal(Track::Type type) const
{
NodeTraverser traverser;
switch (type) {
case Track::kVideo:
if (IsInputConnected(kTextureInput)) {
NodeValueTable t = traverser.GenerateTable(
GetConnectedOutput(kTextureInput), TimeRange(0, 0));
rational r = t.Get(NodeValue::kRational, QStringLiteral("length"))
.toRational();
if (!r.isNaN()) {
return r;
}
}
break;
case Track::kAudio:
if (IsInputConnected(kSamplesInput)) {
NodeValueTable t = traverser.GenerateTable(
GetConnectedOutput(kSamplesInput), TimeRange(0, 0));
rational r = t.Get(NodeValue::kRational, QStringLiteral("length"))
.toRational();
if (!r.isNaN()) {
return r;
}
}
break;
case Track::kNone:
case Track::kSubtitle:
case Track::kCount:
break;
}
return 0;
}
Node *ViewerOutput::GetConnectedTextureOutput()
{
return GetConnectedOutput(kTextureInput);
}
Node::ValueHint ViewerOutput::GetConnectedTextureValueHint()
{
return GetValueHintForInput(kTextureInput);
}
Node *ViewerOutput::GetConnectedSampleOutput()
{
return GetConnectedOutput(kSamplesInput);
}
Node::ValueHint ViewerOutput::GetConnectedSampleValueHint()
{
return GetValueHintForInput(kSamplesInput);
}
void ViewerOutput::SetWaveformEnabled(bool e)
{
if ((waveform_requests_enabled_ = e)) {
if (Node *connected = this->GetConnectedSampleOutput()) {
TimeRange max_range = InputTimeAdjustment(
kSamplesInput, -1, TimeRange(0, GetAudioLength()), false);
TimeRangeList invalid =
connected->waveform_cache()->GetInvalidatedRanges(max_range);
for (const TimeRange &r : invalid) {
connected->waveform_cache()->Request(this, r);
}
}
}
}
void ViewerOutput::Value(const NodeValueRow &value, const NodeGlobals &globals,
NodeValueTable *table) const
{
if (HasInputWithID(kTextureInput)) {
NodeValue repush = value[kTextureInput];
repush.set_tag(Track::Reference(Track::kVideo, 0).ToString());
table->Push(repush);
}
if (HasInputWithID(kSamplesInput)) {
NodeValue repush = value[kSamplesInput];
repush.set_tag(Track::Reference(Track::kAudio, 0).ToString());
table->Push(value[kSamplesInput]);
}
}
bool ViewerOutput::LoadCustom(QXmlStreamReader *reader, SerializedData *data)
{
while (XMLReadNextStartElement(reader)) {
if (reader->name() == QStringLiteral("markers")) {
if (!this->GetMarkers()->load(reader)) {
return false;
}
} else if (reader->name() == QStringLiteral("workarea")) {
if (!this->GetWorkArea()->load(reader)) {
return false;
}
} else {
reader->skipCurrentElement();
}
}
return true;
}
void ViewerOutput::SaveCustom(QXmlStreamWriter *writer) const
{
writer->writeStartElement(QStringLiteral("workarea"));
this->GetWorkArea()->save(writer);
writer->writeEndElement(); // workarea
writer->writeStartElement(QStringLiteral("markers"));
this->GetMarkers()->save(writer);
writer->writeEndElement(); // markers
}
void ViewerOutput::InputValueChangedEvent(const QString &input, int element)
{
if (element == 0) {
if (input == kVideoParamsInput) {
VideoParams new_video_params = GetVideoParams();
bool size_changed =
cached_video_params_.width() != new_video_params.width() ||
cached_video_params_.height() != new_video_params.height();
bool frame_rate_changed = cached_video_params_.frame_rate() !=
new_video_params.frame_rate();
bool pixel_aspect_changed =
cached_video_params_.pixel_aspect_ratio() !=
new_video_params.pixel_aspect_ratio();
bool interlacing_changed = cached_video_params_.interlacing() !=
new_video_params.interlacing();
if (size_changed) {
emit SizeChanged(new_video_params.width(),
new_video_params.height());
}
if (pixel_aspect_changed) {
emit PixelAspectChanged(new_video_params.pixel_aspect_ratio());
}
if (interlacing_changed) {
emit InterlacingChanged(new_video_params.interlacing());
}
if (frame_rate_changed) {
emit FrameRateChanged(new_video_params.frame_rate());
}
emit VideoParamsChanged();
cached_video_params_ = new_video_params;
} else if (input == kAudioParamsInput) {
AudioParams new_audio_params = GetAudioParams();
bool sample_rate_changed = new_audio_params.sample_rate() !=
cached_audio_params_.sample_rate();
if (sample_rate_changed) {
emit SampleRateChanged(new_audio_params.sample_rate());
}
emit AudioParamsChanged();
cached_audio_params_ = new_audio_params;
}
}
super::InputValueChangedEvent(input, element);
}
void ViewerOutput::set_parameters_from_footage(
const QVector<ViewerOutput *> footage)
{
foreach (ViewerOutput *f, footage) {
QVector<VideoParams> video_streams = f->GetEnabledVideoStreams();
QVector<AudioParams> audio_streams = f->GetEnabledAudioStreams();
for (int i = 0; i < video_streams.size(); i++) {
const VideoParams &s = video_streams.at(i);
bool found_video_params = false;
rational using_timebase;
if (s.video_type() == VideoParams::kVideoTypeStill) {
// If this is a still image, we'll use it's resolution but won't set
// `found_video_params` in case something with a frame rate comes along which we'll
// prioritize
if (i > 0) {
// Ignore still images past stream 0
continue;
}
using_timebase = GetVideoParams().time_base();
} else {
using_timebase = s.frame_rate_as_time_base();
found_video_params = true;
}
SetVideoParams(
VideoParams(s.width(), s.height(), using_timebase,
static_cast<PixelFormat::Format>(
OLIVE_CONFIG("OfflinePixelFormat").toInt()),
VideoParams::kInternalChannelCount,
s.pixel_aspect_ratio(), s.interlacing(), 1));
if (found_video_params) {
break;
}
}
if (!audio_streams.isEmpty()) {
const AudioParams &s = audio_streams.first();
SetAudioParams(AudioParams(s.sample_rate(), s.channel_layout(),
kDefaultSampleFormat));
}
}
}
int ViewerOutput::AddStream(Track::Type type, const QVariant &value)
{
return SetStream(type, value, -1);
}
int ViewerOutput::SetStream(Track::Type type, const QVariant &value,
int index_in)
{
QString id;
if (type == Track::kVideo) {
id = kVideoParamsInput;
} else if (type == Track::kAudio) {
id = kAudioParamsInput;
} else if (type == Track::kSubtitle) {
id = kSubtitleParamsInput;
} else {
return -1;
}
// Add another video/audio param to the array for this stream
int index = (index_in == -1) ? InputArraySize(id) : index_in;
if (index >= InputArraySize(id)) {
InputArrayResize(id, index + 1);
}
SetStandardValue(id, value, index);
return index;
}
QVector<VideoParams> ViewerOutput::GetEnabledVideoStreams() const
{
QVector<VideoParams> streams;
int vp_sz = GetVideoStreamCount();
for (int i = 0; i < vp_sz; i++) {
VideoParams vp = GetVideoParams(i);
if (vp.enabled()) {
streams.append(vp);
}
}
return streams;
}
QVector<AudioParams> ViewerOutput::GetEnabledAudioStreams() const
{
QVector<AudioParams> streams;
int ap_sz = GetAudioStreamCount();
for (int i = 0; i < ap_sz; i++) {
AudioParams ap = GetAudioParams(i);
if (ap.enabled()) {
streams.append(ap);
}
}
return streams;
}
}