NazaraEngine/plugins/FFmpeg/Plugin.cpp

505 lines
14 KiB
C++

/*
Nazara Engine - FFmpeg Plugin
Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
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 <https://www.gnu.org/licenses/>
*/
#include <Nazara/Core/Algorithm.hpp>
#include <Nazara/Core/ByteStream.hpp>
#include <Nazara/Utility/ImageStream.hpp>
#include <Nazara/Utility/Utility.hpp>
#include <Nazara/Utility/Plugins/FFmpegPlugin.hpp>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#include <array>
#include <cstring>
namespace
{
class FFmpegStream : public Nz::ImageStream
{
public:
FFmpegStream() :
m_codec(nullptr),
m_codecContext(nullptr),
m_formatContext(nullptr),
m_rawFrame(nullptr),
m_rgbaFrame(nullptr),
m_ioContext(nullptr),
m_conversionContext(nullptr),
m_ioBuffer(nullptr),
m_videoStream(-1)
{
}
~FFmpegStream()
{
if (m_conversionContext)
sws_freeContext(m_conversionContext);
if (m_rawFrame)
av_frame_free(&m_rawFrame);
if (m_rgbaFrame)
av_frame_free(&m_rgbaFrame);
if (m_codecContext)
avcodec_free_context(&m_codecContext);
if (m_rawFrame)
av_frame_free(&m_rawFrame);
if (m_formatContext)
avformat_close_input(&m_formatContext);
if (m_ioContext)
avio_context_free(&m_ioContext);
// m_ioBuffer is freed by avio_close
if (m_ioBuffer)
av_free(&m_ioBuffer);
}
Nz::Result<void, Nz::ResourceLoadingError> Check()
{
constexpr std::size_t BufferSize = 32768;
m_ioBuffer = av_malloc(BufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
m_ioContext = avio_alloc_context(static_cast<unsigned char*>(m_ioBuffer), BufferSize, 0, &m_byteStream, &FFmpegStream::Read, nullptr, &FFmpegStream::Seek);
if (!m_ioContext)
{
NazaraError("failed to create io context");
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
m_formatContext = avformat_alloc_context();
if (!m_formatContext)
{
NazaraError("failed to allocate format context");
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
m_formatContext->pb = m_ioContext;
av_log_set_level(AV_LOG_FATAL);
if (int errCode = avformat_open_input(&m_formatContext, "", nullptr, nullptr); errCode != 0)
{
NazaraErrorFmt("failed to open input: {0}", ErrorToString(errCode));
return Nz::Err(Nz::ResourceLoadingError::Unrecognized);
}
if (int errCode = avformat_find_stream_info(m_formatContext, nullptr); errCode != 0)
{
NazaraErrorFmt("failed to find stream info: {0}", ErrorToString(errCode));
return Nz::Err(Nz::ResourceLoadingError::Unrecognized);
}
m_videoStream = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (m_videoStream < 0)
{
NazaraError("failed to find video stream");
return Nz::Err(Nz::ResourceLoadingError::Unrecognized);
}
if (m_formatContext->streams[m_videoStream]->nb_frames == 0)
{
NazaraError("unhandled 0 frame count");
return Nz::Err(Nz::ResourceLoadingError::Unsupported);
}
m_codec = avcodec_find_decoder(m_formatContext->streams[m_videoStream]->codecpar->codec_id);
if (!m_codec)
{
NazaraError("codec not found");
return Nz::Err(Nz::ResourceLoadingError::Unsupported);
}
return Nz::Ok();
}
bool DecodeNextFrame(void* frameBuffer, Nz::Time* frameTime) override
{
AVPacket packet;
for (;;)
{
if (int errCode = av_read_frame(m_formatContext, &packet); errCode < 0)
{
if (errCode == AVERROR_EOF)
{
if (frameTime)
{
AVRational timebase = m_formatContext->streams[m_videoStream]->time_base;
*frameTime = Nz::Time::Milliseconds(1000 * m_formatContext->streams[m_videoStream]->duration * timebase.num / timebase.den);
}
return false;
}
NazaraErrorFmt("failed to read frame: {0}", ErrorToString(errCode));
return false;
}
if (packet.stream_index != m_videoStream)
continue;
if (int errCode = avcodec_send_packet(m_codecContext, &packet); errCode < 0)
{
NazaraErrorFmt("failed to send packet: {0}", ErrorToString(errCode));
return false;
}
if (int errCode = avcodec_receive_frame(m_codecContext, m_rawFrame); errCode < 0)
{
if (errCode == AVERROR(EAGAIN))
continue;
NazaraErrorFmt("failed to receive frame: {0}", ErrorToString(errCode));
return false;
}
break;
}
sws_scale(m_conversionContext, m_rawFrame->data, m_rawFrame->linesize, 0, m_codecContext->height, m_rgbaFrame->data, m_rgbaFrame->linesize);
Nz::UInt8* dst = static_cast<Nz::UInt8*>(frameBuffer);
Nz::UInt8* src = m_rgbaFrame->data[0];
std::size_t lineSize = m_rgbaFrame->width * 4;
if (lineSize != m_rgbaFrame->linesize[0])
{
for (int i = 0; i < m_rgbaFrame->height; ++i)
{
std::memcpy(dst, src, lineSize);
dst += lineSize;
src += m_rgbaFrame->linesize[0];
}
}
else
std::memcpy(dst, src, lineSize * m_rgbaFrame->height);
if (frameTime)
{
AVRational timebase = m_formatContext->streams[m_videoStream]->time_base;
*frameTime = Nz::Time::Milliseconds(1000 * m_rawFrame->pts * timebase.num / timebase.den);
}
return true;
}
Nz::UInt64 GetFrameCount() const override
{
return m_formatContext->streams[m_videoStream]->nb_frames;
}
Nz::PixelFormat GetPixelFormat() const override
{
return Nz::PixelFormat::RGBA8;
}
Nz::Vector2ui GetSize() const override
{
unsigned int width = Nz::SafeCast<unsigned int>(m_codecContext->width);
unsigned int height = Nz::SafeCast<unsigned int>(m_codecContext->height);
return { width, height };
}
Nz::Result<void, Nz::ResourceLoadingError> Open()
{
auto checkResult = Check();
if (!checkResult)
return checkResult;
const AVCodecParameters* codecParameters = m_formatContext->streams[m_videoStream]->codecpar;
m_codecContext = avcodec_alloc_context3(m_codec);
if (!m_codecContext)
{
NazaraError("failed to allocate codec context");
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
if (int errCode = avcodec_parameters_to_context(m_codecContext, codecParameters); errCode < 0)
{
NazaraErrorFmt("failed to copy codec params to codec context: {0}", ErrorToString(errCode));
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
if (int errCode = avcodec_open2(m_codecContext, m_codec, nullptr); errCode < 0)
{
NazaraErrorFmt("could not open codec: {0}", ErrorToString(errCode));
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
m_rawFrame = av_frame_alloc();
m_rgbaFrame = av_frame_alloc();
if (!m_rawFrame || !m_rgbaFrame)
{
NazaraError("failed to allocate frames");
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
m_rgbaFrame->format = AVPixelFormat::AV_PIX_FMT_RGBA;
m_rgbaFrame->width = codecParameters->width;
m_rgbaFrame->height = codecParameters->height;
if (int errCode = av_frame_get_buffer(m_rgbaFrame, 0); errCode < 0)
{
NazaraErrorFmt("failed to open input: {0}", ErrorToString(errCode));
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
m_conversionContext = sws_getContext(m_codecContext->width, m_codecContext->height, m_codecContext->pix_fmt, m_codecContext->width, m_codecContext->height, AVPixelFormat::AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
if (!m_conversionContext)
{
NazaraError("failed to allocate conversion context");
return Nz::Err(Nz::ResourceLoadingError::Internal);
}
return Nz::Ok();
}
void Seek(Nz::UInt64 frameIndex) override
{
// TODO
avio_seek(m_ioContext, 0, SEEK_SET);
avformat_seek_file(m_formatContext, m_videoStream, std::numeric_limits<Nz::Int64>::min(), 0, std::numeric_limits<Nz::Int64>::max(), 0);
}
bool SetFile(const std::filesystem::path& filePath)
{
std::unique_ptr<Nz::File> file = std::make_unique<Nz::File>();
if (!file->Open(filePath, Nz::OpenMode::Read))
{
NazaraErrorFmt("failed to open stream from file: {0}", Nz::Error::GetLastError());
return false;
}
m_ownedStream = std::move(file);
SetStream(*m_ownedStream);
return true;
}
void SetMemory(const void* data, std::size_t size)
{
m_ownedStream = std::make_unique<Nz::MemoryView>(data, size);
SetStream(*m_ownedStream);
}
void SetStream(Nz::Stream& stream)
{
m_byteStream.SetStream(&stream);
}
Nz::UInt64 Tell() override
{
return 0;
}
private:
static std::string ErrorToString(int errCode)
{
// extract error tag
unsigned int tag = static_cast<unsigned int>(-errCode);
std::string errMessage(6 + AV_ERROR_MAX_STRING_SIZE, ' '); // "ABCD: <error>"
for (std::size_t i = 0; i < 4; ++i)
errMessage[i] = (tag >> i * 8) & 0xFF;
errMessage[4] = ':';
if (av_strerror(errCode, &errMessage[6], AV_ERROR_MAX_STRING_SIZE) == 0)
std::strcpy(&errMessage[6], "<av_strerror failed>");
errMessage.resize(std::strlen(errMessage.data())); //< ew
return errMessage;
}
static int Read(void* opaque, Nz::UInt8* buf, int buf_size)
{
Nz::ByteStream& stream = *static_cast<Nz::ByteStream*>(opaque);
std::size_t readSize = stream.Read(buf, buf_size);
if (readSize == 0)
{
if (stream.GetStream()->EndOfStream())
return AVERROR_EOF;
return -1; //< failure
}
return Nz::SafeCast<int>(readSize);
}
static int64_t Seek(void* opaque, int64_t offset, int whence)
{
Nz::ByteStream& byteStream = *static_cast<Nz::ByteStream*>(opaque);
Nz::Stream* stream = byteStream.GetStream();
if (stream->IsSequential())
return -1;
switch (whence)
{
case SEEK_CUR:
stream->Read(nullptr, static_cast<std::size_t>(offset));
break;
case SEEK_END:
stream->SetCursorPos(stream->GetSize() + offset); // offset is negative here
break;
case SEEK_SET:
stream->SetCursorPos(offset);
break;
case AVSEEK_SIZE:
return stream->GetSize();
default:
NazaraInternalError("Seek mode not handled");
return false;
}
return stream->GetCursorPos();
}
const AVCodec* m_codec;
AVCodecContext* m_codecContext;
AVFormatContext* m_formatContext;
AVFrame* m_rawFrame;
AVFrame* m_rgbaFrame;
AVIOContext* m_ioContext;
SwsContext* m_conversionContext;
void* m_ioBuffer;
std::unique_ptr<Nz::Stream> m_ownedStream;
Nz::ByteStream m_byteStream;
int m_videoStream;
};
bool CheckVideoExtension(std::string_view extension)
{
const AVOutputFormat* format = av_guess_format(nullptr, extension.data(), nullptr);
if (!format)
return false;
return format->video_codec != AV_CODEC_ID_NONE;
}
Nz::Result<std::shared_ptr<Nz::ImageStream>, Nz::ResourceLoadingError> LoadFile(const std::filesystem::path& filePath, const Nz::ImageStreamParams& /*parameters*/)
{
std::shared_ptr<FFmpegStream> ffmpegStream = std::make_shared<FFmpegStream>();
ffmpegStream->SetFile(filePath);
Nz::Result<void, Nz::ResourceLoadingError> status = ffmpegStream->Open();
return status.Map([&] { return std::move(ffmpegStream); });
}
Nz::Result<std::shared_ptr<Nz::ImageStream>, Nz::ResourceLoadingError> LoadMemory(const void* ptr, std::size_t size, const Nz::ImageStreamParams& /*parameters*/)
{
std::shared_ptr<FFmpegStream> ffmpegStream = std::make_shared<FFmpegStream>();
ffmpegStream->SetMemory(ptr, size);
Nz::Result<void, Nz::ResourceLoadingError> status = ffmpegStream->Open();
return status.Map([&] { return std::move(ffmpegStream); });
}
Nz::Result<std::shared_ptr<Nz::ImageStream>, Nz::ResourceLoadingError> LoadStream(Nz::Stream& stream, const Nz::ImageStreamParams& /*parameters*/)
{
std::shared_ptr<FFmpegStream> ffmpegStream = std::make_shared<FFmpegStream>();
ffmpegStream->SetStream(stream);
Nz::Result<void, Nz::ResourceLoadingError> status = ffmpegStream->Open();
return status.Map([&] { return std::move(ffmpegStream); });
}
class FFmpegPluginImpl final : public Nz::FFmpegPlugin
{
public:
bool Activate() override
{
Nz::Utility* utility = Nz::Utility::Instance();
NazaraAssert(utility, "utility module is not instancied");
Nz::ImageStreamLoader::Entry loaderEntry;
loaderEntry.extensionSupport = CheckVideoExtension;
loaderEntry.fileLoader = LoadFile;
loaderEntry.memoryLoader = LoadMemory;
loaderEntry.streamLoader = LoadStream;
loaderEntry.parameterFilter = [](const Nz::ImageStreamParams& parameters)
{
if (auto result = parameters.custom.GetBooleanParameter("SkipFFMpegLoader"); result.GetValueOr(false))
return false;
return true;
};
Nz::ImageStreamLoader& imageStreamLoader = utility->GetImageStreamLoader();
m_ffmpegLoaderEntry = imageStreamLoader.RegisterLoader(loaderEntry);
return true;
}
void Deactivate() override
{
Nz::Utility* utility = Nz::Utility::Instance();
NazaraAssert(utility, "utility module is not instanced");
Nz::ImageStreamLoader& imageStreamLoader = utility->GetImageStreamLoader();
imageStreamLoader.UnregisterLoader(m_ffmpegLoaderEntry);
m_ffmpegLoaderEntry = nullptr;
}
std::string_view GetDescription() const override
{
return "Adds supports to load and decode videos streams";
}
std::string_view GetName() const override
{
return "FFMpeg loader";
}
Nz::UInt32 GetVersion() const override
{
return 100;
}
private:
const Nz::ImageStreamLoader::Entry* m_ffmpegLoaderEntry = nullptr;
};
}
extern "C"
{
NAZARA_EXPORT Nz::PluginInterface* PluginLoad()
{
Nz::Utility* utility = Nz::Utility::Instance();
if (!utility)
{
NazaraError("Utility module must be initialized");
return nullptr;
}
std::unique_ptr<FFmpegPluginImpl> plugin = std::make_unique<FFmpegPluginImpl>();
return plugin.release();
}
}