Skip to content

Commit 2b9e643

Browse files
committed
fix: finalize the embedded ffmpeg encoder
1 parent 58999f2 commit 2b9e643

File tree

2 files changed

+183
-88
lines changed

2 files changed

+183
-88
lines changed

src/graphics/video_renderer_embedded.cpp

Lines changed: 169 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@
33
#include "helper/c_helpers.hpp"
44
#include "helper/constants.hpp"
55
#include "helper/git_helper.hpp"
6+
#include "helper/graphic_utils.hpp"
67
#include "video_renderer.hpp"
78

89
extern "C" {
910
#include <libavcodec/avcodec.h>
1011
#include <libavformat/avformat.h>
1112
#include <libavutil/avutil.h>
1213
#include <libavutil/log.h>
14+
#include <libswscale/swscale.h>
1315
}
1416

1517
#include <csignal>
18+
#include <future>
1619
#include <sys/wait.h>
1720
#include <unistd.h>
1821

1922
struct Decoder {
2023
int pipe;
21-
pid_t pid;
24+
std::future<std::optional<std::string>> encoding_thread;
25+
std::atomic<bool> should_cancel;
2226
};
2327

2428
// general information and usage from: https://friendlyuser.github.io/posts/tech/cpp/Using_FFmpeg_in_C++_A_Comprehensive_Guide/
@@ -51,8 +55,13 @@ namespace {
5155
return result;
5256
}
5357

54-
std::optional<std::string>
55-
start_encoding(u32 fps, shapes::UPoint size, const std::filesystem::path& destination_path) {
58+
std::optional<std::string> start_encoding(
59+
u32 fps,
60+
shapes::UPoint size,
61+
const std::filesystem::path& destination_path,
62+
int input_fd,
63+
const std::unique_ptr<Decoder>& decoder
64+
) {
5665

5766
ScopeDeferMultiple<void, void*> scope_defer{};
5867

@@ -66,8 +75,6 @@ namespace {
6675
return fmt::format("Cannot allocate an input format context");
6776
}
6877

69-
scope_defer.add([input_format_ctx](void*) { avformat_free_context(input_format_ctx); }, nullptr);
70-
7178
const std::string resolution = fmt::format("{}x{}", size.x, size.y);
7279

7380
const std::string framerate = fmt::format("{}", fps);
@@ -87,8 +94,10 @@ namespace {
8794
// "-r {framerate}"
8895
av_dict_set(&input_options, "framerate", framerate.c_str(), 0);
8996

90-
// "-i -"
91-
auto av_input_ret = avformat_open_input(&input_format_ctx, "fd:", input_fmt, &input_options);
97+
std::string input_url = fmt::format("pipe:{}", input_fd);
98+
99+
// "-i pipe:{fd}"
100+
auto av_input_ret = avformat_open_input(&input_format_ctx, input_url.c_str(), input_fmt, &input_options);
92101
if (av_input_ret != 0) {
93102
return fmt::format("Could not open input file stdin: {}", av_error_to_string(av_input_ret));
94103
}
@@ -108,7 +117,7 @@ namespace {
108117
}
109118

110119

111-
/* select the video stream */
120+
// select the video stream
112121
const AVCodec* input_decoder = nullptr;
113122
auto video_stream_index = av_find_best_stream(input_format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &input_decoder, 0);
114123
if (video_stream_index < 0) {
@@ -129,23 +138,23 @@ namespace {
129138
auto codec_paramaters_ret = avcodec_parameters_to_context(input_codec_context, input_video_stream->codecpar);
130139
if (codec_paramaters_ret < 0) {
131140
return fmt::format(
132-
"Cannot set the input codec context paramaters: {}", av_error_to_string(codec_paramaters_ret)
141+
"Cannot set the input codec context parameters: {}", av_error_to_string(codec_paramaters_ret)
133142
);
134143
}
135144

136145
/* Inform the decoder about the timebase for the packet timestamps.
137146
* This is highly recommended, but not mandatory. */
138147
input_codec_context->pkt_timebase = input_video_stream->time_base;
139148

140-
//NOTE: we also could set this to the provided u32 , but this also uses that and converts it to the expected format
149+
//NOTE: we also could set this to the provided u32, but this also uses that and converts it to the expected format (fractional)
141150
input_codec_context->framerate = av_guess_frame_rate(input_format_ctx, input_video_stream, nullptr);
142151

143152
auto codec_open_ret = avcodec_open2(input_codec_context, input_decoder, nullptr);
144153
if (codec_open_ret != 0) {
145154
return fmt::format("Cannot initializer the codec for the input: {}", av_error_to_string(codec_open_ret));
146155
}
147156

148-
av_dump_format(input_format_ctx, 0, "fd:", 0);
157+
av_dump_format(input_format_ctx, 0, input_url.c_str(), 0);
149158

150159
// output setup
151160

@@ -192,8 +201,9 @@ namespace {
192201
output_codec_context->height = input_codec_context->height;
193202
output_codec_context->width = input_codec_context->width;
194203
output_codec_context->sample_aspect_ratio = input_codec_context->sample_aspect_ratio;
204+
output_codec_context->framerate = input_codec_context->framerate;
195205

196-
/* video time_base can be set to whatever is handy and supported by encoder */
206+
// video time_base can be set to whatever is handy and supported by encoder
197207
output_codec_context->time_base = av_inv_q(input_codec_context->framerate);
198208

199209
AVDictionary* output_options = nullptr;
@@ -202,6 +212,9 @@ namespace {
202212
// "-crf 20"
203213
av_dict_set(&output_options, "crf", "20", 0);
204214

215+
av_dict_set(&output_options, "video_size", resolution.c_str(), 0);
216+
217+
205218
auto codec_open_out_ret = avcodec_open2(output_codec_context, output_encoder, &output_options);
206219
if (codec_open_out_ret != 0) {
207220
return fmt::format(
@@ -260,47 +273,143 @@ namespace {
260273

261274
scope_defer.add([&pkt](void*) { av_packet_free(&pkt); }, nullptr);
262275

276+
AVFrame* decode_frame = av_frame_alloc();
277+
278+
if (decode_frame == nullptr) {
279+
return "Could not allocate decode AVFrame";
280+
}
281+
282+
scope_defer.add([&decode_frame](void*) { av_frame_free(&decode_frame); }, nullptr);
283+
284+
285+
decode_frame->format = input_codec_context->pix_fmt;
286+
decode_frame->width = input_codec_context->width;
287+
decode_frame->height = input_codec_context->height;
288+
289+
auto frame_buffer_ret = av_frame_get_buffer(decode_frame, 0);
290+
if (frame_buffer_ret < 0) {
291+
return fmt::format("Could not allocate decode frame buffer: {}", av_error_to_string(frame_buffer_ret));
292+
}
293+
294+
AVFrame* encode_frame = av_frame_alloc();
295+
296+
if (encode_frame == nullptr) {
297+
return "Could not allocate encode AVFrame";
298+
}
299+
300+
scope_defer.add([&encode_frame](void*) { av_frame_free(&encode_frame); }, nullptr);
301+
302+
303+
encode_frame->format = output_codec_context->pix_fmt;
304+
encode_frame->width = output_codec_context->width;
305+
encode_frame->height = output_codec_context->height;
306+
307+
auto outp_frame_buffer_ret = av_frame_get_buffer(encode_frame, 0);
308+
if (outp_frame_buffer_ret < 0) {
309+
return fmt::format("Could not allocate encode frame buffer: {}", av_error_to_string(outp_frame_buffer_ret));
310+
}
311+
312+
// allocate conversion context (for frame conversion)
313+
SwsContext* sws_ctx = sws_getContext(
314+
input_codec_context->width, input_codec_context->height, input_codec_context->pix_fmt,
315+
output_codec_context->width, output_codec_context->height, output_codec_context->pix_fmt, SWS_BICUBIC,
316+
nullptr, nullptr, nullptr
317+
);
318+
if (sws_ctx == nullptr) {
319+
return "Could not allocate conversion context";
320+
}
321+
263322
while (true) {
264-
auto read_ret = av_read_frame(input_format_ctx, pkt);
265-
if (read_ret < 0)
266-
break;
323+
// check atomic bool, if we are cancelled
324+
// NOTE: the video is garbage after this, since we don't close it correctly (which isn't the intention of this)
325+
if (decoder->should_cancel) {
326+
return std::nullopt;
327+
}
267328

329+
// retrieve unencoded (raw) packet from input
330+
auto read_frame_ret = av_read_frame(input_format_ctx, pkt);
331+
if (read_frame_ret == AVERROR_EOF) {
332+
break;
333+
} else if (read_frame_ret < 0) {
334+
return fmt::format("Receiving a frame from the input failed: {}", av_error_to_string(read_frame_ret));
335+
}
268336

269-
auto send_pkt_ret = avcodec_send_packet(output_codec_context, pkt);
270-
if (send_pkt_ret < 0) {
337+
// send raw packet in packet to decoder
338+
auto send_pkt_ret = avcodec_send_packet(input_codec_context, pkt);
339+
if (send_pkt_ret != 0) {
340+
if (send_pkt_ret == AVERROR(EAGAIN)) {
341+
return "Decoding failed: Output was not read correctly";
342+
}
271343
return fmt::format("Decoding failed: {}", av_error_to_string(send_pkt_ret));
272344
}
273345

274-
int write_ret = 0;
346+
int read_ret = 0;
275347

276-
while (write_ret >= 0) {
277-
write_ret = avcodec_receive_packet(output_codec_context, pkt);
278-
if (write_ret == AVERROR(EAGAIN) || write_ret == AVERROR_EOF) {
348+
// encode and write as much frames as possible
349+
while (read_ret >= 0) {
350+
351+
// get decoded frame, if one is present
352+
read_ret = avcodec_receive_frame(input_codec_context, decode_frame);
353+
if (read_ret == AVERROR(EAGAIN) || read_ret == AVERROR_EOF) {
279354
break;
355+
} else if (read_ret < 0) {
356+
return fmt::format("Receiving a frame from the decoder failed: {}", av_error_to_string(read_ret));
357+
}
280358

281-
} else if (write_ret < 0) {
282-
return fmt::format("Encoding a frame failed: {}", av_error_to_string(write_ret));
359+
// convert to correct output pixel format
360+
read_ret = sws_scale_frame(sws_ctx, encode_frame, decode_frame);
361+
if (read_ret < 0) {
362+
return fmt::format("Frame conversion failed: {}", av_error_to_string(read_ret));
283363
}
284364

285-
/* rescale output packet timestamp values from codec to stream timebase */
286-
av_packet_rescale_ts(pkt, output_codec_context->time_base, out_stream->time_base);
287-
pkt->stream_index = out_stream->index;
365+
// copy the pts from the decoded frame
366+
encode_frame->pts = decode_frame->pts;
288367

289-
/* Write the compressed frame to the media file. */
290-
write_ret = av_interleaved_write_frame(output_format_ctx, pkt);
291-
/* pkt is now blank (av_interleaved_write_frame() takes ownership of
368+
// encode decoded and converted frame with output encoder
369+
read_ret = avcodec_send_frame(output_codec_context, encode_frame);
370+
if (read_ret != 0) {
371+
return fmt::format("Encoding failed: {}", av_error_to_string(read_ret));
372+
}
373+
374+
int write_ret = 0;
375+
376+
// write all encoded packets
377+
while (write_ret >= 0) {
378+
379+
// get encoded packet, if one is present
380+
write_ret = avcodec_receive_packet(output_codec_context, pkt);
381+
if (write_ret == AVERROR(EAGAIN) || write_ret == AVERROR_EOF) {
382+
break;
383+
} else if (write_ret < 0) {
384+
return fmt::format(
385+
"Receiving a packet from the encoder failed: {}", av_error_to_string(write_ret)
386+
);
387+
}
388+
389+
// prepare packet for muxing
390+
pkt->stream_index = out_stream->index;
391+
392+
// rescale output packet timestamp values from codec to stream timebase
393+
av_packet_rescale_ts(pkt, output_codec_context->time_base, out_stream->time_base);
394+
395+
396+
// Write the compressed packet (frame inside that) to the media file.
397+
write_ret = av_interleaved_write_frame(output_format_ctx, pkt);
398+
/* pkt is now blank (av_interleaved_write_frame() takes ownership of
292399
* its contents and resets pkt), so that no unreferencing is necessary.
293400
* This would be different if one used av_write_frame(). */
294-
if (write_ret < 0) {
295-
return fmt::format("Writing an output packet failed: {}", av_error_to_string(write_ret));
401+
if (write_ret < 0) {
402+
return fmt::format("Writing an output packet failed: {}", av_error_to_string(write_ret));
403+
}
296404
}
297405
}
298-
299-
// reset the packe, so that it's ready for the next cycle
300-
av_packet_unref(pkt);
301406
}
302407

303-
av_write_trailer(output_format_ctx);
408+
409+
auto trailer_ret = av_write_trailer(output_format_ctx);
410+
if (trailer_ret != 0) {
411+
return fmt::format("Writing the trailer failed: {}", av_error_to_string(trailer_ret));
412+
}
304413

305414
return std::nullopt;
306415
}
@@ -317,79 +426,53 @@ std::optional<std::string> VideoRendererBackend::setup(u32 fps, shapes::UPoint s
317426
std::array<int, 2> pipefd = { 0, 0 };
318427

319428
if (pipe(pipefd.data()) < 0) {
320-
return fmt::format("FFMPEG: Could not create a pipe: {}", strerror(errno));
429+
return fmt::format("Could not create a pipe: {}", strerror(errno));
321430
}
322431

323-
pid_t child = fork();
324-
if (child < 0) {
325-
return fmt::format("FFMPEG: could not fork a child: {}", strerror(errno));
326-
}
432+
std::future<std::optional<std::string>> encoding_thread =
433+
std::async(std::launch::async, [pipefd, fps, size, this] -> std::optional<std::string> {
434+
utils::set_thread_name("ffmpeg encoder");
435+
auto result = start_encoding(fps, size, this->m_destination_path, pipefd[READ_END], this->m_decoder);
327436

328-
if (child == 0) {
329-
if (dup2(pipefd.at(READ_END), STDIN_FILENO) < 0) {
330-
std::cerr << "FFMPEG CHILD: could not reopen read end of pipe as stdin: " << strerror(errno) << "\n";
331-
std::exit(1);
332-
}
333-
close(pipefd[WRITE_END]);
334-
335-
auto result = start_encoding(fps, size, m_destination_path);
437+
if (close(pipefd[READ_END]) < 0) {
438+
spdlog::warn("could not close read end of the pipe: {}", strerror(errno));
439+
}
336440

337-
if (result.has_value()) {
338-
std::cerr << "FFMPEG CHILD: could not run embedded ffmpeg as a child process: " << result.value() << "\n";
339-
std::exit(1);
340-
}
441+
if (result.has_value()) {
442+
return fmt::format("ffmpeg error: {}", result.value());
443+
}
341444

342-
std::exit(0);
343-
}
445+
return std::nullopt;
446+
});
344447

345-
if (close(pipefd[READ_END]) < 0) {
346-
spdlog::error("FFMPEG: could not close read end of the pipe on the parent's end: {}", strerror(errno));
347-
}
348448

349-
m_decoder = std::make_unique<Decoder>(pipefd[WRITE_END], child);
449+
m_decoder = std::make_unique<Decoder>(pipefd[WRITE_END], std::move(encoding_thread), false);
350450
return std::nullopt;
351451
}
352452

353453
bool VideoRendererBackend::add_frame(SDL_Surface* surface) {
354454
if (write(m_decoder->pipe, surface->pixels, static_cast<size_t>(surface->h) * surface->pitch) < 0) {
355-
spdlog::error("FFMPEG: failed to write into ffmpeg pipe: {}", strerror(errno));
455+
spdlog::error("failed to write into ffmpeg pipe: {}", strerror(errno));
356456
return false;
357457
}
358458
return true;
359459
}
360460

361461
bool VideoRendererBackend::finish(bool cancel) {
362462

463+
if (cancel) {
464+
m_decoder->should_cancel = true;
465+
}
363466

364467
if (close(m_decoder->pipe) < 0) {
365-
spdlog::warn("FFMPEG: could not close write end of the pipe on the parent's end: {}", strerror(errno));
468+
spdlog::warn("could not close write end of the pipe: {}", strerror(errno));
366469
}
367470

368-
if (cancel)
369-
kill(m_decoder->pid, SIGKILL);
370-
371-
while (true) {
372-
int wstatus = 0;
373-
if (waitpid(m_decoder->pid, &wstatus, 0) < 0) {
374-
spdlog::error("FFMPEG: could not wait for ffmpeg child process to finish: {}", strerror(errno));
375-
return false;
376-
}
377-
378-
if (WIFEXITED(wstatus)) {
379-
int exit_status = WEXITSTATUS(wstatus);
380-
if (exit_status != 0) {
381-
spdlog::error("FFMPEG: ffmpeg exited with code {}", exit_status);
382-
return false;
383-
}
384-
385-
return true;
386-
}
387-
388-
if (WIFSIGNALED(wstatus)) {
389-
spdlog::error("FFMPEG: ffmpeg got terminated by {}", strsignal(WTERMSIG(wstatus)));
390-
return false;
391-
}
471+
m_decoder->encoding_thread.wait();
472+
auto result = m_decoder->encoding_thread.get();
473+
if (result.has_value()) {
474+
spdlog::error("FFMPEG error: {}", result.value());
475+
return false;
392476
}
393-
394-
UNREACHABLE();
477+
return true;
395478
}

0 commit comments

Comments
 (0)