From d335c5d73c41fefbf1960eb60eff5fbd3929c0c8 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Thu, 19 May 2022 12:51:26 +0200 Subject: [PATCH] Add FFmpeg plugin --- .github/workflows/coverage.yml | 2 +- .github/workflows/linux-build.yml | 6 +- .github/workflows/macos-build.yml | 18 +- .github/workflows/msys2-build.yml | 6 +- .github/workflows/windows-build.yml | 6 +- include/Nazara/Core/Enums.hpp | 1 + plugins/FFmpeg/Plugin.cpp | 493 ++++++++++++++++++++++++++++ plugins/FFmpeg/xmake.lua | 23 ++ 8 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 plugins/FFmpeg/Plugin.cpp create mode 100644 plugins/FFmpeg/xmake.lua diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3800e7ee7..25b38b74e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -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 diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 3768bdbd4..478538441 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -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 diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 82baa447c..2eb524139 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -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 diff --git a/.github/workflows/msys2-build.yml b/.github/workflows/msys2-build.yml index 36efcd117..9772af00f 100644 --- a/.github/workflows/msys2-build.yml +++ b/.github/workflows/msys2-build.yml @@ -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 diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 7f2a2e5b6..2c45d8db9 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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 diff --git a/include/Nazara/Core/Enums.hpp b/include/Nazara/Core/Enums.hpp index a8b7dfd27..fd8459561 100644 --- a/include/Nazara/Core/Enums.hpp +++ b/include/Nazara/Core/Enums.hpp @@ -135,6 +135,7 @@ namespace Nz enum class Plugin { Assimp, + FFmpeg, Max = Assimp }; diff --git a/plugins/FFmpeg/Plugin.cpp b/plugins/FFmpeg/Plugin.cpp new file mode 100644 index 000000000..4c6323f03 --- /dev/null +++ b/plugins/FFmpeg/Plugin.cpp @@ -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 +#include +#include +#include + +extern "C" +{ + #include + #include + #include + #include +} + +#include +#include + +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(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(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(m_codecContext->width); + unsigned int height = Nz::SafeCast(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::min(), 0, std::numeric_limits::max(), 0); + } + + bool SetFile(const std::filesystem::path& filePath) + { + std::unique_ptr file = std::make_unique(); + 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(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(-errCode); + + std::string errMessage(6 + AV_ERROR_MAX_STRING_SIZE, ' '); // "ABCD: " + 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], ""); + + 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(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(readSize); + } + + static int64_t Seek(void* opaque, int64_t offset, int whence) + { + Nz::ByteStream& byteStream = *static_cast(opaque); + Nz::Stream* stream = byteStream.GetStream(); + + if (stream->IsSequential()) + return -1; + + switch (whence) + { + case SEEK_CUR: + stream->Read(nullptr, static_cast(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 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 LoadFile(const std::filesystem::path& filePath, const Nz::ImageStreamParams& /*parameters*/) + { + std::shared_ptr ffmpegStream = std::make_shared(); + ffmpegStream->SetFile(filePath); + + if (!ffmpegStream->Open()) + return {}; + + return ffmpegStream; + } + + std::shared_ptr LoadMemory(const void* ptr, std::size_t size, const Nz::ImageStreamParams& /*parameters*/) + { + std::shared_ptr ffmpegStream = std::make_shared(); + ffmpegStream->SetMemory(ptr, size); + + if (!ffmpegStream->Open()) + return {}; + + return ffmpegStream; + } + + std::shared_ptr LoadStream(Nz::Stream& stream, const Nz::ImageStreamParams& /*parameters*/) + { + std::shared_ptr ffmpegStream = std::make_shared(); + 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; + } +} diff --git a/plugins/FFmpeg/xmake.lua b/plugins/FFmpeg/xmake.lua new file mode 100644 index 000000000..02ba2d3c5 --- /dev/null +++ b/plugins/FFmpeg/xmake.lua @@ -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