/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 .
*
*/
//
// Created by mikesolar on 25-10-1.
//
#include "OliveClip.h"
#include "common/Current.h"
#include "common/ffmpegutils.h"
#include "ofxCore.h"
#include "ofxhClip.h"
#include "pluginSupport/image.h"
#include
#include
#include
#include
#ifdef OFX_SUPPORTS_OPENGLRENDER
#include
#endif
extern "C" {
#include
}
namespace {
const std::string kBitDepthNoneStr(kOfxBitDepthNone);
const std::string kBitDepthByteStr(kOfxBitDepthByte);
const std::string kBitDepthShortStr(kOfxBitDepthShort);
const std::string kBitDepthHalfStr(kOfxBitDepthHalf);
const std::string kBitDepthFloatStr(kOfxBitDepthFloat);
const std::string kImageComponentNoneStr(kOfxImageComponentNone);
const std::string kImageComponentAlphaStr(kOfxImageComponentAlpha);
const std::string kImageComponentRGBStr(kOfxImageComponentRGB);
const std::string kImageComponentRGBAStr(kOfxImageComponentRGBA);
const std::string kImagePremultStr(kOfxImagePreMultiplied);
const std::string kImageUnPremultStr(kOfxImageUnPreMultiplied);
const std::string kImageFieldNoneStr(kOfxImageFieldNone);
const std::string kImageFieldUpperStr(kOfxImageFieldUpper);
const std::string kImageFieldLowerStr(kOfxImageFieldLower);
}
const std::string &olive::plugin::OliveClipInstance::getUnmappedBitDepth() const
{
switch (params_.format()) {
case PixelFormat::INVALID:
return kBitDepthNoneStr;
case PixelFormat::U8:
return kBitDepthByteStr;
case PixelFormat::U16:
return kBitDepthShortStr;
case PixelFormat::F16:
return kBitDepthHalfStr;
case PixelFormat::F32:
return kBitDepthFloatStr;
default:
return kBitDepthNoneStr;
}
}
const std::string &
olive::plugin::OliveClipInstance::getUnmappedComponents() const
{
switch (params_.channel_count()) {
case 1:
return kImageComponentAlphaStr;
case 3:
return kImageComponentRGBStr;
case 4:
return kImageComponentRGBAStr;
default:
return kImageComponentNoneStr;
}
}
const std::string &olive::plugin::OliveClipInstance::getPremult() const
{
if (params_.premultiplied_alpha()) {
return kImagePremultStr;
} else {
return kImageUnPremultStr;
}
}
double olive::plugin::OliveClipInstance::getAspectRatio() const
{
return params_.pixel_aspect_ratio().toDouble();
}
double olive::plugin::OliveClipInstance::getFrameRate() const
{
return params_.frame_rate().toDouble();
}
void olive::plugin::OliveClipInstance::getFrameRange(double &startFrame,
double &endFrame) const
{
startFrame = params_.frame_rate().toDouble() * params_.start_time();
endFrame =
startFrame + params_.frame_rate().toDouble() * params_.duration();
}
const std::string &olive::plugin::OliveClipInstance::getFieldOrder() const
{
switch (params_.interlacing()) {
case VideoParams::kInterlaceNone:
return kImageFieldNoneStr;
case VideoParams::kInterlacedTopFirst:
return kImageFieldUpperStr;
case VideoParams::kInterlacedBottomFirst:
return kImageFieldLowerStr;
}
return kImageFieldNoneStr;
}
bool olive::plugin::OliveClipInstance::getConnected() const
{
if (name_ == kOfxImageEffectOutputClipName) {
return true;
}
#ifdef OFX_SUPPORTS_OPENGLRENDER
if (!input_textures_.isEmpty()) {
return true;
}
#endif
return !images_.isEmpty();
}
double olive::plugin::OliveClipInstance::getUnmappedFrameRate() const
{
return getFrameRate();
}
void olive::plugin::OliveClipInstance::getUnmappedFrameRange(
double &startFrame, double &endFrame) const
{
getFrameRange(startFrame, endFrame);
}
bool olive::plugin::OliveClipInstance::getContinuousSamples() const
{
return false;
}
OFX::Host::ImageEffect::Image *
olive::plugin::OliveClipInstance::getImage(OfxTime time,
const OfxRectD *optionalBounds)
{
OfxRectD rod_d = getRegionOfDefinition(time);
OfxRectI rod = { static_cast(std::floor(rod_d.x1)),
static_cast(std::floor(rod_d.y1)),
static_cast(std::ceil(rod_d.x2)),
static_cast(std::ceil(rod_d.y2)) };
OfxRectI bounds = rod;
if (optionalBounds) {
bounds.x1 = static_cast(std::floor(optionalBounds->x1));
bounds.y1 = static_cast(std::floor(optionalBounds->y1));
bounds.x2 = static_cast(std::ceil(optionalBounds->x2));
bounds.y2 = static_cast(std::ceil(optionalBounds->y2));
}
// Clamp bounds to ROD to keep host/plugin coords consistent.
bounds.x1 = std::max(bounds.x1, rod.x1);
bounds.y1 = std::max(bounds.y1, rod.y1);
bounds.x2 = std::min(bounds.x2, rod.x2);
bounds.y2 = std::min(bounds.y2, rod.y2);
if (name_ == "Output") {
if (!images_.contains(time)) {
// make a new ref counted image
images_.insert(time, std::make_shared(*const_cast(this),
params_, bounds, rod, true));
}
// add another reference to the member image for this fetch
// as we have a ref count of 1 due to construction, this will
// cause the output image never to delete by the plugin
// when it releases the image
images_[time]->addReference();
images_[time]->EnsureAllocatedFromParams(params_, bounds, rod, true);
// return it
return images_[time].get();
} else {
if (images_.contains(time)) {
std::shared_ptr image = images_.value(time);
image->EnsureAllocatedFromParams(params_, bounds, rod, false);
image->addReference();
return image.get();
}
// Fetch on demand for the input clip.
// It does get deleted after the plugin is done with it as we
// have not incremented the auto ref
//
// You should do somewhat more sophisticated image management
// than this.
Image *image = new Image(*this, params_, bounds, rod, true);
return image;
}
}
std::shared_ptr
olive::plugin::OliveClipInstance::getOutputImage(OfxTime time)
{
if (images_.contains(time)) {
return images_.value(time);
}
OfxRectD rod_d = getRegionOfDefinition(time);
OfxRectI rod = { static_cast(std::floor(rod_d.x1)),
static_cast(std::floor(rod_d.y1)),
static_cast(std::ceil(rod_d.x2)),
static_cast(std::ceil(rod_d.y2)) };
OfxRectI bounds = rod;
auto image = std::make_shared(*this, params_, bounds, rod, true);
images_.insert(time, image);
return image;
}
OfxRectD
olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const
{
if (regionOfDefinitions_.contains(time)) {
return regionOfDefinitions_[time];
}
OfxRectD regionOfDefinition;
regionOfDefinition.x1 = regionOfDefinition.y1 = 0;
regionOfDefinition.x2 = params_.width();
regionOfDefinition.y2 = params_.height();
return regionOfDefinition;
}
void olive::plugin::OliveClipInstance::setRegionOfDefinition(
OfxRectD regionOfDefinition, OfxTime time)
{
regionOfDefinitions_[time] = regionOfDefinition;
}
void olive::plugin::OliveClipInstance::setDefaultRegionOfDefinition(
OfxRectD regionOfDefinition)
{
defaultRegionOfDefinitions_ = regionOfDefinition;
}
void olive::plugin::OliveClipInstance::setInputTexture(TexturePtr texture, OfxTime time){
if (!texture) {
return;
}
VideoParams incoming = texture->params();
if (params_.format() != PixelFormat::INVALID &&
params_.channel_count() > 0) {
incoming.set_format(params_.format());
incoming.set_channel_count(params_.channel_count());
incoming.set_premultiplied_alpha(params_.premultiplied_alpha());
}
this->params_ = incoming;
#ifdef OFX_SUPPORTS_OPENGLRENDER
input_textures_.insert(time, texture);
#endif
AVFramePtr frame = texture->frame();
if (!frame || !frame->data[0]) {
return;
}
AVPixelFormat expected_fmt =
FFmpegUtils::GetFFmpegPixelFormat(params_.format(),
params_.channel_count());
if (expected_fmt == AV_PIX_FMT_NONE) {
return;
}
OfxRectI bounds = { 0, 0, params_.width(), params_.height() };
OfxRectD rod_d = getRegionOfDefinition(time);
OfxRectI regionOfDefinition = { static_cast(std::floor(rod_d.x1)),
static_cast(std::floor(rod_d.y1)),
static_cast(std::ceil(rod_d.x2)),
static_cast(std::ceil(rod_d.y2)) };
std::shared_ptr image;
if (images_.contains(time)) {
image = images_.value(time);
image->EnsureAllocatedFromParams(params_, bounds, regionOfDefinition,
false);
} else {
image = std::make_shared(*this, params_, bounds,
regionOfDefinition, false);
images_.insert(time, image);
}
uint8_t *dst = image->data();
if (!dst) {
return;
}
AVFramePtr src_frame = frame;
if (frame->format != expected_fmt ||
frame->width != params_.width() ||
frame->height != params_.height()) {
AVFramePtr converted = CreateAVFramePtr();
converted->format = expected_fmt;
converted->width = params_.width();
converted->height = params_.height();
if (av_frame_get_buffer(converted.get(), 0) < 0) {
return;
}
SwsContext *sws_ctx = sws_getContext(
frame->width, frame->height,
static_cast(frame->format),
converted->width, converted->height,
static_cast(converted->format),
SWS_POINT, nullptr, nullptr, nullptr);
if (!sws_ctx) {
return;
}
sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height,
converted->data, converted->linesize);
sws_freeContext(sws_ctx);
src_frame = converted;
}
int bytes_per_component = params_.format().byte_count();
int bytes_per_row = params_.width() * params_.channel_count() *
bytes_per_component;
int src_row_bytes = src_frame->linesize[0];
int dst_row_bytes = image->row_bytes();
int copy_bytes = std::min(bytes_per_row,
std::min(src_row_bytes, dst_row_bytes));
int copy_height = std::min(image->height(), src_frame->height);
const uint8_t *src = src_frame->data[0];
for (int y = 0; y < copy_height; ++y) {
std::memcpy(dst + y * dst_row_bytes, src + y * src_row_bytes,
copy_bytes);
}
}
void olive::plugin::OliveClipInstance::setOutputTexture(Texture *texture,
OfxTime time)
{
#ifdef OFX_SUPPORTS_OPENGLRENDER
if (!texture) {
return;
}
output_textures_.insert(time, texture);
#else
(void)texture;
(void)time;
#endif
}
#ifdef OFX_SUPPORTS_OPENGLRENDER
OFX::Host::ImageEffect::Texture *
olive::plugin::OliveClipInstance::loadTexture(OfxTime time, const char *format,
const OfxRectD *optionalBounds)
{
(void)format;
Texture *gl_texture = nullptr;
if (isOutput()) {
gl_texture = output_textures_.value(time, nullptr);
} else {
TexturePtr input = input_textures_.value(time);
gl_texture = input ? input.get() : nullptr;
}
if (!gl_texture || gl_texture->IsDummy() || !gl_texture->id().isValid()) {
return nullptr;
}
OfxRectD rod_d = getRegionOfDefinition(time);
OfxRectI rod = { static_cast(std::floor(rod_d.x1)),
static_cast(std::floor(rod_d.y1)),
static_cast(std::ceil(rod_d.x2)),
static_cast(std::ceil(rod_d.y2)) };
OfxRectI bounds = rod;
if (optionalBounds) {
bounds.x1 = static_cast(std::floor(optionalBounds->x1));
bounds.y1 = static_cast(std::floor(optionalBounds->y1));
bounds.x2 = static_cast(std::ceil(optionalBounds->x2));
bounds.y2 = static_cast(std::ceil(optionalBounds->y2));
}
bounds.x1 = std::max(bounds.x1, rod.x1);
bounds.y1 = std::max(bounds.y1, rod.y1);
bounds.x2 = std::min(bounds.x2, rod.x2);
bounds.y2 = std::min(bounds.y2, rod.y2);
const int bytes_per_row =
params_.width() * params_.channel_count() * params_.format().byte_count();
const std::string &field = getFieldOrder();
const std::string unique_id = std::to_string(
reinterpret_cast(gl_texture)) + "_" +
std::to_string(static_cast(time));
const int texture_id = gl_texture->id().value();
OFX::Host::ImageEffect::Texture *texture =
new OFX::Host::ImageEffect::Texture(
*this, 1.0, 1.0, texture_id, GL_TEXTURE_2D, bounds, rod,
bytes_per_row, field, unique_id);
texture->addReference();
return texture;
}
#endif