Add FFmpeg plugin

This commit is contained in:
SirLynix 2022-05-19 12:51:26 +02:00 committed by Jérôme Leclercq
parent ca23942d36
commit d335c5d73c
8 changed files with 544 additions and 11 deletions

View File

@ -89,7 +89,7 @@ jobs:
# Setup compilation mode and install project dependencies
- name: Configure xmake and install dependencies
run: xmake config --ccache=n --shadernodes=y --tests=y --unitybuild=y --arch=${{ matrix.config.arch }} --mode=coverage --yes
run: xmake config --arch=${{ matrix.config.arch }} --mode=coverage --ffmpeg=y --shadernodes=y --tests=y --unitybuild=y --yes
# Build the engine
- name: Build Nazara

View File

@ -68,7 +68,7 @@ jobs:
# Setup compilation mode and install project dependencies
- name: Configure xmake and install dependencies
run: xmake config --ccache=n --shadernodes=y --tests=y --unitybuild=y --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ccache=n --ffmpeg=y --shadernodes=y --tests=y --unitybuild=y --yes
# Build the engine
- name: Build Nazara
@ -79,6 +79,10 @@ jobs:
if: matrix.mode != 'releasedbg'
run: xmake run NazaraUnitTests
# Setup installation configuration
- name: Configure xmake for installation
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ffmpeg=n --shadernodes=y --tests=y --yes
# Install the result files
- name: Install Nazara
run: xmake install -vo package

View File

@ -62,7 +62,7 @@ jobs:
# Setup compilation mode and install project dependencies
- name: Configure xmake and install dependencies
run: xmake config --ccache=n --shadernodes=y --tests=y --unitybuild=y --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ccache=n --ffmpeg=y --shadernodes=y --tests=y --unitybuild=y --yes
# Build the engine
- name: Build Nazara
@ -73,12 +73,16 @@ jobs:
if: matrix.mode != 'releasedbg'
run: xmake run NazaraUnitTests
# Setup installation configuration
- name: Configure xmake for installation
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ffmpeg=n --shadernodes=y --tests=y --yes
# Install the result files
# - name: Install Nazara
# run: xmake install -vo package
- name: Install Nazara
run: xmake install -vo package
# Upload artifacts
# - uses: actions/upload-artifact@v2
# with:
# name: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.mode }}
# path: package
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.mode }}
path: package

View File

@ -92,7 +92,7 @@ jobs:
# Setup compilation mode and install project dependencies
- name: Configure xmake and install dependencies
run: xmake config --ccache=n --shadernodes=y --tests=y --unitybuild=y --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ccache=n --ffmpeg=y --shadernodes=y --tests=y --unitybuild=y --yes
# Build the engine
- name: Build Nazara
@ -103,6 +103,10 @@ jobs:
if: matrix.mode != 'releasedbg'
run: xmake run NazaraUnitTests
# Setup installation configuration
- name: Configure xmake for installation
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ffmpeg=n --shadernodes=y --tests=y --yes
# Install the result files
- name: Install Nazara
run: xmake install -vo package

View File

@ -62,7 +62,7 @@ jobs:
# Setup compilation mode and install project dependencies
- name: Configure xmake and install dependencies
run: xmake config --ccache=n --shadernodes=y --tests=y --unitybuild=y --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ccache=n --ffmpeg=y --shadernodes=y --tests=y --unitybuild=y --yes
# Build the engine
- name: Build Nazara
@ -73,6 +73,10 @@ jobs:
if: matrix.mode != 'releasedbg'
run: xmake run NazaraUnitTests
# Setup installation configuration
- name: Configure xmake for installation
run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --ffmpeg=n --shadernodes=y --tests=y --yes
# Install the result files
- name: Install Nazara
run: xmake install -vo package

View File

@ -135,6 +135,7 @@ namespace Nz
enum class Plugin
{
Assimp,
FFmpeg,
Max = Assimp
};

493
plugins/FFmpeg/Plugin.cpp Normal file
View File

@ -0,0 +1,493 @@
/*
Nazara Engine - FFmpeg Plugin
Copyright (C) 2015 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <Nazara/Core/Algorithm.hpp>
#include <Nazara/Core/ByteStream.hpp>
#include <Nazara/Utility/ImageStream.hpp>
#include <Nazara/Utility/Utility.hpp>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#include <array>
#include <cstring>
namespace
{
const Nz::ImageStreamLoader::Entry* ffmpegLoaderEntry = nullptr;
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);
}
bool 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 false;
}
m_formatContext = avformat_alloc_context();
if (!m_formatContext)
{
NazaraError("failed to allocate format context");
return false;
}
m_formatContext->pb = m_ioContext;
av_log_set_level(AV_LOG_FATAL);
if (int errCode = avformat_open_input(&m_formatContext, "", nullptr, nullptr); errCode != 0)
{
NazaraError("failed to open input: " + ErrorToString(errCode));
return false;
}
if (int errCode = avformat_find_stream_info(m_formatContext, nullptr); errCode != 0)
{
NazaraError("failed to find stream info: " + ErrorToString(errCode));
return false;
}
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 false;
}
if (m_formatContext->streams[m_videoStream]->nb_frames == 0)
{
NazaraError("unhandled 0 frame count");
return false;
}
m_codec = avcodec_find_decoder(m_formatContext->streams[m_videoStream]->codecpar->codec_id);
if (!m_codec)
{
NazaraError("codec not found");
return false;
}
return true;
}
bool DecodeNextFrame(void* frameBuffer, Nz::UInt64* 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 = 1000 * m_formatContext->streams[m_videoStream]->duration * timebase.num / timebase.den;
}
return false;
}
NazaraError("failed to read frame: " + ErrorToString(errCode));
return false;
}
if (packet.stream_index != m_videoStream)
continue;
if (int errCode = avcodec_send_packet(m_codecContext, &packet); errCode < 0)
{
NazaraError("failed to send packet: " + ErrorToString(errCode));
return false;
}
if (int errCode = avcodec_receive_frame(m_codecContext, m_rawFrame); errCode < 0)
{
if (errCode == AVERROR(EAGAIN))
continue;
NazaraError("failed to receive frame: " + 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 = 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 };
}
bool Open()
{
if (!Check())
{
NazaraError("stream has invalid GIF header");
return false;
}
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 false;
}
if (int errCode = avcodec_parameters_to_context(m_codecContext, codecParameters); errCode < 0)
{
NazaraError("failed to copy codec params to codec context: " + ErrorToString(errCode));
return false;
}
if (int errCode = avcodec_open2(m_codecContext, m_codec, nullptr); errCode < 0)
{
NazaraError("could not open codec: " + ErrorToString(errCode));
return false;
}
m_rawFrame = av_frame_alloc();
m_rgbaFrame = av_frame_alloc();
if (!m_rawFrame || !m_rgbaFrame)
{
NazaraError("failed to allocate frames");
return false;
}
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)
{
NazaraError("failed to open input: " + ErrorToString(errCode));
return false;
}
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 false;
}
return true;
}
void Seek(Nz::UInt64 frameIndex) override
{
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::ReadOnly))
{
NazaraError("Failed to open stream from file: " + 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(const std::string_view& extension)
{
return extension == "mp4";
}
Nz::Ternary CheckVideo(Nz::Stream& stream, const Nz::ImageStreamParams& parameters)
{
bool skip;
if (parameters.custom.GetBooleanParameter("SkipFFMpegLoader", &skip) && skip)
return Nz::Ternary::False;
FFmpegStream ffmpegStream;
ffmpegStream.SetStream(stream);
if (ffmpegStream.Check())
return Nz::Ternary::True;
else
return Nz::Ternary::False;
}
std::shared_ptr<Nz::ImageStream> LoadFile(const std::filesystem::path& filePath, const Nz::ImageStreamParams& /*parameters*/)
{
std::shared_ptr<FFmpegStream> ffmpegStream = std::make_shared<FFmpegStream>();
ffmpegStream->SetFile(filePath);
if (!ffmpegStream->Open())
return {};
return ffmpegStream;
}
std::shared_ptr<Nz::ImageStream> 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);
if (!ffmpegStream->Open())
return {};
return ffmpegStream;
}
std::shared_ptr<Nz::ImageStream> LoadStream(Nz::Stream& stream, const Nz::ImageStreamParams& /*parameters*/)
{
std::shared_ptr<FFmpegStream> ffmpegStream = std::make_shared<FFmpegStream>();
ffmpegStream->SetStream(stream);
if (!ffmpegStream->Open())
return {};
return ffmpegStream;
}
}
extern "C"
{
NAZARA_EXPORT int PluginLoad()
{
Nz::Utility* utility = Nz::Utility::Instance();
NazaraAssert(utility, "utility module is not instancied");
Nz::ImageStreamLoader::Entry loaderEntry;
loaderEntry.extensionSupport = CheckVideoExtension;
loaderEntry.streamChecker = CheckVideo;
loaderEntry.fileLoader = LoadFile;
loaderEntry.memoryLoader = LoadMemory;
loaderEntry.streamLoader = LoadStream;
Nz::ImageStreamLoader& imageStreamLoader = utility->GetImageStreamLoader();
ffmpegLoaderEntry = imageStreamLoader.RegisterLoader(loaderEntry);
return 1;
}
NAZARA_EXPORT void PluginUnload()
{
Nz::Utility* utility = Nz::Utility::Instance();
NazaraAssert(utility, "utility module is not instancied");
Nz::ImageStreamLoader& imageStreamLoader = utility->GetImageStreamLoader();
imageStreamLoader.UnregisterLoader(ffmpegLoaderEntry);
ffmpegLoaderEntry = nullptr;
}
}

23
plugins/FFmpeg/xmake.lua Normal file
View File

@ -0,0 +1,23 @@
option("ffmpeg")
set_default(false)
set_showmenu(true)
set_description("Build FFmpeg plugin")
option_end()
if has_config("ffmpeg") then
add_requires("ffmpeg", { configs = { shared = true } })
target("PluginFFmpeg")
set_kind("shared")
set_group("Plugins")
add_rpathdirs("$ORIGIN")
add_deps("NazaraUtility")
add_packages("ffmpeg")
add_headerfiles("**.hpp")
add_headerfiles("**.inl")
add_includedirs(".")
add_files("**.cpp")
end