|
| 1 | + |
| 2 | +#include "video_renderer.hpp" |
| 3 | + |
| 4 | +#define WIN32_LEAN_AND_MEAN |
| 5 | +#define NOMINMAX |
| 6 | + |
| 7 | +#include <windows.h> |
| 8 | + |
| 9 | + |
| 10 | +struct Decoder { |
| 11 | + HANDLE hProcess; |
| 12 | + HANDLE hPipeWrite; |
| 13 | +}; |
| 14 | + |
| 15 | +// inspired by: https://github.com/tsoding/musializer/blob/762a729ff69ba1f984b0f2604e0eac08af46327c/src/ffmpeg_windows.c |
| 16 | +VideoRendererBackend::VideoRendererBackend(const std::filesystem::path& destination_path) |
| 17 | + : m_destination_path{ destination_path }, |
| 18 | + m_decoder{ nullptr } { } |
| 19 | + |
| 20 | +VideoRendererBackend::~VideoRendererBackend() = default; |
| 21 | + |
| 22 | + |
| 23 | +std::optional<std::string> VideoRendererBackend::setup(u32 fps, shapes::UPoint size) { |
| 24 | + |
| 25 | + HANDLE pipe_read; |
| 26 | + HANDLE pipe_write; |
| 27 | + |
| 28 | + SECURITY_ATTRIBUTES saAttr = { 0 }; |
| 29 | + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); |
| 30 | + saAttr.bInheritHandle = TRUE; |
| 31 | + |
| 32 | + if (!CreatePipe(&pipe_read, &pipe_write, &saAttr, 0)) { |
| 33 | + return fmt::format("FFMPEG: Could not create pipe. System Error Code: {}", GetLastError()); |
| 34 | + } |
| 35 | + |
| 36 | + if (!SetHandleInformation(pipe_write, HANDLE_FLAG_INHERIT, 0)) { |
| 37 | + return fmt::format( |
| 38 | + "FFMPEG: Could not mark write pipe as non-inheritable. System Error Code: {}", GetLastError() |
| 39 | + ); |
| 40 | + } |
| 41 | + |
| 42 | + // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output |
| 43 | + |
| 44 | + STARTUPINFO siStartInfo; |
| 45 | + ZeroMemory(&siStartInfo, sizeof(siStartInfo)); |
| 46 | + siStartInfo.cb = sizeof(STARTUPINFO); |
| 47 | + // NOTE: theoretically setting NULL to std handles should not be a problem |
| 48 | + // https://docs.microsoft.com/en-us/windows/console/getstdhandle?redirectedfrom=MSDN#attachdetach-behavior |
| 49 | + siStartInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); |
| 50 | + if (siStartInfo.hStdError == INVALID_HANDLE_VALUE) { |
| 51 | + return fmt::format( |
| 52 | + "FFMPEG: Could get standard error handle for the child. System Error Code: {}", GetLastError() |
| 53 | + ); |
| 54 | + } |
| 55 | + siStartInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); |
| 56 | + if (siStartInfo.hStdOutput == INVALID_HANDLE_VALUE) { |
| 57 | + return fmt::format( |
| 58 | + "FFMPEG: Could get standard output handle for the child. System Error Code: {}", GetLastError() |
| 59 | + ); |
| 60 | + } |
| 61 | + siStartInfo.hStdInput = pipe_read; |
| 62 | + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; |
| 63 | + |
| 64 | + PROCESS_INFORMATION piProcInfo; |
| 65 | + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); |
| 66 | + |
| 67 | + //TODO: do this in better c++ fashion and deduplicate this |
| 68 | + char cmd_buffer[1024 * 2]; |
| 69 | + snprintf( |
| 70 | + cmd_buffer, sizeof(cmd_buffer), |
| 71 | + "ffmpeg.exe -loglevel verbose -y -f rawvideo -pix_fmt rgba -s %dx%d -r %d -i - -i \"%s\" -c:v libx264 -vb " |
| 72 | + "2500k -c:a aac -ab 200k -pix_fmt yuv420p output.mp4", |
| 73 | + (int) width, (int) height, (int) fps, sound_file_path |
| 74 | + ); |
| 75 | + |
| 76 | + if (!CreateProcess(NULL, cmd_buffer, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo)) { |
| 77 | + CloseHandle(pipe_write); |
| 78 | + CloseHandle(pipe_read); |
| 79 | + |
| 80 | + return fmt::format("FFMPEG: Could not create child process. System Error Code: {}", GetLastError()); |
| 81 | + } |
| 82 | + |
| 83 | + CloseHandle(pipe_read); |
| 84 | + CloseHandle(piProcInfo.hThread); |
| 85 | + |
| 86 | + m_decoder = std::make_unique<Decoder>(piProcInfo.hProcess, pipe_write); |
| 87 | + return std::nullopt; |
| 88 | +} |
| 89 | + |
| 90 | +bool VideoRendererBackend::add_frame(SDL_Surface* surface) { |
| 91 | + DWORD written{}; |
| 92 | + |
| 93 | + if (not WriteFile( |
| 94 | + m_decoder->hPipeWrite, surface->pixels, static_cast<size_t>(surface->h) * surface->pitch, &written, NULL |
| 95 | + )) { |
| 96 | + spdlog::error("FFMPEG: failed to write into ffmpeg pipe. System Error Code: {}", GetLastError()); |
| 97 | + return false; |
| 98 | + } |
| 99 | + return true; |
| 100 | +} |
| 101 | + |
| 102 | +bool VideoRendererBackend::finish(bool cancel) { |
| 103 | + |
| 104 | + FlushFileBuffers(m_decoder->hPipeWrite); |
| 105 | + CloseHandle(m_decoder->hPipeWrite); |
| 106 | + |
| 107 | + if (cancel) { |
| 108 | + TerminateProcess(m_decoder->hProcess, 1); |
| 109 | + } |
| 110 | + |
| 111 | + if (WaitForSingleObject(m_decoder->hProcess, INFINITE) == WAIT_FAILED) { |
| 112 | + spdlog::error("FFMPEG: could not wait on child process. System Error Code: {}", GetLastError()); |
| 113 | + CloseHandle(m_decoder->hProcess); |
| 114 | + return false; |
| 115 | + } |
| 116 | + |
| 117 | + DWORD exit_status{}; |
| 118 | + if (GetExitCodeProcess(m_decoder->hProcess, &exit_status) == 0) { |
| 119 | + spdlog::error("FFMPEG: could not get process exit code. System Error Code: {}", GetLastError()); |
| 120 | + CloseHandle(m_decoder->hProcess); |
| 121 | + return false; |
| 122 | + } |
| 123 | + |
| 124 | + if (exit_status != 0) { |
| 125 | + spdlog::error("FFMPEG: command exited with exit code {}", exit_status); |
| 126 | + CloseHandle(m_decoder->hProcess); |
| 127 | + return false; |
| 128 | + } |
| 129 | + |
| 130 | + CloseHandle(m_decoder->hProcess); |
| 131 | + |
| 132 | + return true; |
| 133 | +} |
0 commit comments