]> git.localhorst.tv Git - ffmpeg-test.git/commitdiff
basic cairo
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 22 Sep 2024 19:29:47 +0000 (21:29 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 22 Sep 2024 19:29:47 +0000 (21:29 +0200)
.gitignore
Makefile
src/app/Stream.h [new file with mode: 0644]
src/cairo/Context.h [new file with mode: 0644]
src/cairo/Error.h [new file with mode: 0644]
src/cairo/Surface.h [new file with mode: 0644]
src/ffmpeg/CodecContext.h
src/ffmpeg/Error.h
src/ffmpeg/Frame.h
src/ffmpeg/Stream.h
src/main.cpp

index 2c38f592641e405936500812d226c416056c66d3..b499675dd3713d67dd4952f5c8d423147e462395 100644 (file)
@@ -1,4 +1,5 @@
 .gdb_history
+compile_flags.txt
 main
 out.flv
 test.mp4
index 5f363fbd7991da27689f79fde684422a8766cb87..48f4d5c90c410ca776bfa8b77c165fce15f0b7b1 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,16 @@
 CPP_SRCS = $(shell find src -name \*.cpp)
 CPP_DEPS = $(shell find src -name \*.h)
 
+LIBS = cairo libavformat libavcodec libavutil libswresample libswscale
+
 main: $(CPP_SRCS) $(CPP_DEPS)
-       clang++ -g $(shell pkg-config -cflags -libs libavformat libavcodec libavutil libswresample libswscale) $(CPP_SRCS) -o $@
+       clang++ -g $(shell pkg-config --cflags --libs $(LIBS)) $(CPP_SRCS) -o $@
+
+compile_flags.txt:
+       echo -xc++ > $@
+       pkg-config --cflags --libs $(LIBS) | xargs printf '%s\n' >> $@
 
 run: main
        ./main
 
-.PHONY: run
+.PHONY: compile_flags.txt run
diff --git a/src/app/Stream.h b/src/app/Stream.h
new file mode 100644 (file)
index 0000000..5da2a54
--- /dev/null
@@ -0,0 +1,182 @@
+#ifndef TEST_APP_STREAM_H_
+#define TEST_APP_STREAM_H_
+
+#include <chrono>
+#include <cstdint>
+
+extern "C" {
+#include <libavcodec/codec_id.h>
+#include <libavutil/mathematics.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/rational.h>
+#include <libavutil/samplefmt.h>
+}
+
+#include "../ffmpeg/Encoder.h"
+#include "../ffmpeg/OutputContext.h"
+#include "../ffmpeg/Resampler.h"
+#include "../ffmpeg/Scaler.h"
+
+namespace app {
+
+class Stream {
+
+public:
+       Stream(const char *url, int width, int height, int fps)
+       : output(url, "flv")
+       , audio_encoder(AV_CODEC_ID_AAC)
+       , video_encoder(AV_CODEC_ID_H264)
+       , scaler(width, height, AV_PIX_FMT_BGRA, width, height, video_encoder.GetPreferredPixelFormat())
+       , resampler(2, 44100, AV_SAMPLE_FMT_S16, 2, 44100, audio_encoder.GetPreferredSampleFormat()) {
+               audio_encoder.SetDefaultChannelLayout(2);
+               audio_encoder.SetSampleRate(44100);
+               audio_encoder.InferSampleFormat();
+               audio_encoder.SetBitRate(160 * 1000);
+               audio_encoder.Open();
+
+               video_encoder.SetSize(width, height);
+               video_encoder.InferPixelFormat();
+               video_encoder.SetMaxBitRate(6 * 1000 * 1000);
+               video_encoder.SetBufSize(12 * 1000 * 1000);
+               video_encoder.SetFrameRate(AVRational{fps, 1});
+               video_encoder.SetGopSize(2 * fps);
+               video_encoder.SetSampleAspectRatio(AVRational{1, 1});
+               if (output.RequiresGlobalHeader()) {
+                       video_encoder.SetGlobalHeader();
+               }
+               video_encoder.Open();
+
+               video_stream = output.CreateVideoStream(video_encoder);
+               audio_stream = output.CreateAudioStream(audio_encoder);
+
+               video_input_frame.AllocateImage(width, height, AV_PIX_FMT_BGRA);
+               video_encoder.AllocateVideoFrame(video_output_frame);
+
+               audio_input_frame.AllocateAudio(audio_encoder.GetFrameSize(), AV_SAMPLE_FMT_S16, audio_encoder.GetChannelLayout());
+               audio_encoder.AllocateAudioFrame(audio_output_frame);
+       }
+
+       Stream(const Stream &) = delete;
+       Stream &operator =(const Stream &) = delete;
+
+public:
+       uint8_t *GetVideoPlane() {
+               return video_input_frame.GetDataPlane(0);
+       }
+
+       int GetVideoLineSize() const {
+               return video_input_frame.GetPlaneLinesize(0);
+       }
+
+       int16_t *GetAudioPlane() {
+               return reinterpret_cast<int16_t *>(audio_input_frame.GetDataPlane(0));
+       }
+
+       int GetAudioChannels() const {
+               return audio_encoder.GetChannelLayout().nb_channels;
+       }
+
+       int GetAudioFrameSize() const {
+               return audio_encoder.GetFrameSize();
+       }
+
+       void Start() {
+               output.Open();
+               output.WriteHeader();
+               start = std::chrono::high_resolution_clock::now();
+               video_frame_counter = 0;
+               audio_frame_counter = 0;
+       }
+
+       void Finish() {
+               video_encoder.Flush();
+               while (video_encoder.ReceivePacket(video_packet)) {
+                       video_packet.Rescale(video_encoder, video_stream);
+                       video_packet.SetStream(video_stream);
+                       output.WriteInterleavedPacket(video_packet);
+               }
+
+               audio_encoder.Flush();
+               while (audio_encoder.ReceivePacket(audio_packet)) {
+                       audio_packet.Rescale(audio_encoder, audio_stream);
+                       audio_packet.SetStream(audio_stream);
+                       output.WriteInterleavedPacket(audio_packet);
+               }
+
+               output.WriteTrailer();
+       }
+
+       int64_t GetVideoFrameCounter() const {
+               return video_frame_counter;
+       }
+
+       int64_t AudioElapsedMS() const {
+               return av_rescale_q(audio_frame_counter, audio_encoder.GetTimeBase(), AVRational{1, 1000});
+       }
+
+       int64_t TimeElapsedMS() const {
+               const auto now = std::chrono::high_resolution_clock::now();
+               return std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
+       }
+
+       int64_t VideoElapsedMS() const {
+               return av_rescale_q(video_frame_counter, video_encoder.GetTimeBase(), AVRational{1, 1000});
+       }
+
+       void PrepareAudioFrame() {
+               audio_input_frame.MakeWritable();
+       }
+
+       void PushAudioFrame() {
+               resampler.Convert(audio_encoder, audio_input_frame, audio_output_frame);
+               audio_output_frame.SetPresentationTimestamp(audio_frame_counter);
+               audio_encoder.SendFrame(audio_output_frame);
+               while (audio_encoder.ReceivePacket(audio_packet)) {
+                       audio_packet.Rescale(audio_encoder, audio_stream);
+                       audio_packet.SetStream(audio_stream);
+                       output.WriteInterleavedPacket(audio_packet);
+               }
+               audio_frame_counter += audio_encoder.GetFrameSize();
+       }
+
+       void PrepareVideoFrame() {
+               video_input_frame.MakeWritable();
+               video_input_frame.SetSampleAspectRatio(AVRational{1, 1});
+       }
+
+       void PushVideoFrame() {
+               scaler.ScaleFrame(video_input_frame, video_output_frame);
+               video_output_frame.SetPresentationTimestamp(video_frame_counter);
+               video_encoder.SendFrame(video_output_frame);
+               ++video_frame_counter;
+               while (video_encoder.ReceivePacket(video_packet)) {
+                       video_packet.Rescale(video_encoder, video_stream);
+                       video_packet.SetStream(video_stream);
+                       output.WriteInterleavedPacket(video_packet);
+               }
+       }
+
+private:
+       ffmpeg::OutputContext output;
+       ffmpeg::Encoder audio_encoder;
+       ffmpeg::Encoder video_encoder;
+       ffmpeg::Stream audio_stream;
+       ffmpeg::Stream video_stream;
+       ffmpeg::Scaler scaler;
+       ffmpeg::Resampler resampler;
+       ffmpeg::Frame video_input_frame;
+       ffmpeg::Frame video_output_frame;
+       ffmpeg::Packet video_packet;
+       ffmpeg::Frame audio_input_frame;
+       ffmpeg::Frame audio_output_frame;
+       ffmpeg::Packet audio_packet;
+
+       std::chrono::time_point<std::chrono::system_clock> start;
+       int64_t video_frame_counter;
+       int64_t audio_frame_counter;
+
+};
+
+}
+
+#endif
diff --git a/src/cairo/Context.h b/src/cairo/Context.h
new file mode 100644 (file)
index 0000000..92cd24a
--- /dev/null
@@ -0,0 +1,95 @@
+#ifndef TEST_CAIRO_CONTEXT_H_
+#define TEST_CAIRO_CONTEXT_H_
+
+#include <cairo.h>
+#include <iostream>
+#include <ostream>
+
+#include "Error.h"
+
+namespace cairo {
+
+class Context {
+
+public:
+       explicit Context(cairo_surface_t *surface): ctx(cairo_create(surface)) {
+               cairo_status_t res = cairo_status(ctx);
+               if (res != CAIRO_STATUS_SUCCESS) {
+                       throw Error("unable to create context", res);
+               }
+       }
+       ~Context() {
+               cairo_destroy(ctx);
+       }
+       Context(Context &&other): ctx(cairo_reference(other.ctx)) {
+       }
+
+       Context(const Context &) = delete;
+       Context &operator =(const Context &) = delete;
+
+public:
+       void DebugPrint() {
+               cairo_status_t status = cairo_status(ctx);
+               std::cout << "cairo status: " << cairo_status_to_string(status) << std::endl;
+       }
+
+       void Fill() {
+               cairo_fill(ctx);
+       }
+
+       void MoveTo(double x, double y) {
+               cairo_move_to(ctx, x, y);
+       }
+
+       void LineTo(double x, double y) {
+               cairo_line_to(ctx, x, y);
+       }
+
+       void Paint() {
+               cairo_paint(ctx);
+       }
+
+       void Paint(double alpha) {
+               cairo_paint_with_alpha(ctx, alpha);
+       }
+
+       void Rectangle(double x, double y, double w, double h) {
+               cairo_rectangle(ctx, x, y, w, h);
+       }
+
+       void SelectFontFace(const char *family, cairo_font_slant_t slant, cairo_font_weight_t weight) {
+               cairo_select_font_face(ctx, family, slant, weight);
+       }
+
+       void SetFontSize(double size) {
+               cairo_set_font_size(ctx, size);
+       }
+
+       void SetLineWidth(double width) {
+               cairo_set_line_width(ctx, width);
+       }
+
+       void SetSourceRGB(double r, double g, double b) {
+               cairo_set_source_rgb(ctx, r, g, b);
+       }
+
+       void SetSourceRGBA(double r, double g, double b, double a) {
+               cairo_set_source_rgba(ctx, r, g, b, a);
+       }
+
+       void ShowText(const char *text) {
+               cairo_show_text(ctx, text);
+       }
+
+       void Stroke() {
+               cairo_stroke(ctx);
+       }
+
+private:
+       cairo_t *ctx;
+
+};
+
+}
+
+#endif
diff --git a/src/cairo/Error.h b/src/cairo/Error.h
new file mode 100644 (file)
index 0000000..a0b869b
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef TEST_CAIRO_ERROR_H_
+#define TEST_CAIRO_ERROR_H_
+
+#include <stdexcept>
+#include <string>
+
+#include <cairo.h>
+
+namespace {
+std::string make_message(const std::string &msg, cairo_status_t status) {
+       return msg + ": " + std::string(cairo_status_to_string(status));
+}
+}
+
+namespace cairo {
+
+class Error: public std::runtime_error {
+
+public:
+       Error(const std::string &msg, cairo_status_t status): std::runtime_error(make_message(msg, status)) {
+       }
+       ~Error() {
+       }
+
+};
+
+}
+
+#endif
diff --git a/src/cairo/Surface.h b/src/cairo/Surface.h
new file mode 100644 (file)
index 0000000..8e3a15b
--- /dev/null
@@ -0,0 +1,57 @@
+#ifndef TEST_CAIRO_SURFACE_H_
+#define TEST_CAIRO_SURFACE_H_
+
+#include <utility>
+
+#include <cairo.h>
+
+#include "Context.h"
+#include "Error.h"
+
+namespace cairo {
+
+class Surface {
+
+public:
+       Surface(cairo_format_t pixfmt, int w, int h)
+       : s(cairo_image_surface_create(pixfmt, w, h)) {
+               cairo_status_t res = cairo_surface_status(s);
+               if (res != CAIRO_STATUS_SUCCESS) {
+                       throw Error("unable to create surface", res);
+               }
+       }
+       Surface(unsigned char *data, int stride, cairo_format_t pixfmt, int w, int h)
+       : s(cairo_image_surface_create_for_data(data, pixfmt, w, h, stride)) {
+               cairo_status_t res = cairo_surface_status(s);
+               if (res != CAIRO_STATUS_SUCCESS) {
+                       throw Error("unable to create surface", res);
+               }
+       }
+       ~Surface() {
+               cairo_surface_destroy(s);
+       }
+
+       Surface(const Surface &) = delete;
+       Surface &operator =(const Surface &) = delete;
+
+public:
+       Context CreateContext() {
+               return std::move(Context(s));
+       }
+
+       void Flush() {
+               cairo_surface_flush(s);
+       }
+
+       const unsigned char *GetData() const {
+               return cairo_image_surface_get_data(s);
+       }
+
+private:
+       cairo_surface_t *s;
+
+};
+
+}
+
+#endif
index b22b26633cd2cc9b172eb0416e11f5790ac69739..0a8ceece36f304a617f8d549d233ae746981a216 100644 (file)
@@ -80,6 +80,14 @@ public:
                return ctx->time_base;
        }
 
+       AVPixelFormat GetPreferredPixelFormat() const {
+               return codec->pix_fmts[0];
+       }
+
+       AVSampleFormat GetPreferredSampleFormat() const {
+               return codec->sample_fmts[0];
+       }
+
        void InferPixelFormat() {
                ctx->pix_fmt = codec->pix_fmts[0];
        }
index eb664250642dba5d55f8ed274b7c61636178469b..bdcbdcef98c7beade804d0ad4d0211507899d595 100644 (file)
@@ -1,10 +1,13 @@
 #ifndef TEST_FFMPEG_ERROR_H_
 #define TEST_FFMPEG_ERROR_H_
 
-#include <libavutil/error.h>
 #include <stdexcept>
 #include <string>
 
+extern "C" {
+#include <libavutil/error.h>
+}
+
 namespace {
 std::string make_message(const std::string &msg, int code) {
        char buf[128] = {0};
index efc03d8eeb6975acd2da367b1be2df85ac798879..836d07d75a6ad5bb38e8000b80ec3428b1d38020 100644 (file)
@@ -101,7 +101,7 @@ public:
        }
 
 
-       const int GetPlaneLinesize(unsigned int num) {
+       int GetPlaneLinesize(unsigned int num) const {
                if (num >= 8) {
                        throw std::runtime_error("plane index out of bounds");
                }
index 420c372cf42c6890cdb03391e3777f38d23f40c7..88ec5d74ae3289c6e1b4ba49f2f129b7088fe142 100644 (file)
@@ -8,14 +8,13 @@ extern "C" {
 #include <libavutil/rational.h>
 }
 
-#include "CodecContext.h"
-#include "Error.h"
-
 namespace ffmpeg {
 
 class Stream {
 
 public:
+       Stream(): s(nullptr) {
+       }
        explicit Stream(AVStream *s): s(s) {
                if (!s) {
                        throw std::runtime_error("failed to allocate stream");
@@ -26,8 +25,12 @@ public:
        Stream(Stream &&other): s(other.s) {
        }
 
-       Stream(const Stream &) = delete;
-       Stream &operator =(const Stream &) = delete;
+       Stream(const Stream &other): s(other.s) {
+       }
+       Stream &operator =(const Stream &other) {
+               s = other.s;
+               return *this;
+       }
 
 public:
        int GetIndex() const {
index b4f4489c44722c2a669b75eb246b4876493f7f00..a77f4d00da79d0dce9de160065f35e1c2de9265d 100644 (file)
@@ -1,3 +1,5 @@
+#include "cairo.h"
+#include "cairo/Context.h"
 #include <chrono>
 #include <csignal>
 #include <cstdint>
@@ -17,149 +19,85 @@ extern "C" {
 #include <libavutil/timestamp.h>
 }
 
-#include "ffmpeg/Encoder.h"
+#include "app/Stream.h"
+
+#include "cairo/Surface.h"
+
 #include "ffmpeg/Error.h"
-#include "ffmpeg/Frame.h"
-#include "ffmpeg/OutputContext.h"
-#include "ffmpeg/Packet.h"
-#include "ffmpeg/Resampler.h"
-#include "ffmpeg/Scaler.h"
 
 
 namespace {
 
-std::string str_timebase(int64_t num, AVRational tb) {
-       return std::string(av_ts2timestr(num, &tb));
-}
-
 bool running = false;
 
 void stop(int) {
        running = false;
 }
 
+void RenderFrame(int64_t num, int64_t time_ms, cairo::Context &c) {
+       c.SetSourceRGB(0, 0, 0);
+       c.Paint();
+}
+
 }
 
 
 int main(int argc, char**argv) {
        const int WIDTH = 1280;
        const int HEIGHT = 720;
+       const int FPS = 60;
+       const char *URL = "rtmp://localhost/localhorsttv";
 
        int res = avformat_network_init();
        if (res != 0) {
                throw ffmpeg::Error("network init failed", res);
        }
 
+       app::Stream stream(URL, WIDTH, HEIGHT, FPS);
+
        running = true;
        signal(SIGINT, stop);
 
-       ffmpeg::OutputContext output("rtmp://localhost/localhorsttv", "flv");
-
-       ffmpeg::Encoder audio_encoder(AV_CODEC_ID_AAC);
-       audio_encoder.SetDefaultChannelLayout(2);
-       audio_encoder.SetSampleRate(44100);
-       audio_encoder.InferSampleFormat();
-       audio_encoder.SetBitRate(160 * 1000);
-       audio_encoder.Open();
-
-       ffmpeg::Encoder video_encoder(AV_CODEC_ID_H264);
-       video_encoder.SetSize(WIDTH, HEIGHT);
-       video_encoder.InferPixelFormat();
-       video_encoder.SetMaxBitRate(6 * 1000 * 1000);
-       video_encoder.SetBufSize(12 * 1000 * 1000);
-       video_encoder.SetFrameRate(AVRational{60, 1});
-       video_encoder.SetGopSize(120);
-       video_encoder.SetSampleAspectRatio(AVRational{1, 1});
-       if (output.RequiresGlobalHeader()) {
-               video_encoder.SetGlobalHeader();
-       }
-       video_encoder.Open();
-
-       ffmpeg::Stream video_stream = output.CreateVideoStream(video_encoder);
-       ffmpeg::Stream audio_stream = output.CreateAudioStream(audio_encoder);
-
-       ffmpeg::Scaler scaler(WIDTH, HEIGHT, AV_PIX_FMT_RGB24, WIDTH, HEIGHT, video_encoder.GetPixelFormat());
-       ffmpeg::Resampler resampler(audio_encoder.GetChannelLayout().nb_channels, audio_encoder.GetSampleRate(), AV_SAMPLE_FMT_S16, audio_encoder.GetChannelLayout().nb_channels, audio_encoder.GetSampleRate(), audio_encoder.GetSampleFormat());
-
-       ffmpeg::Frame video_input_frame;
-       video_input_frame.AllocateImage(WIDTH, HEIGHT, AV_PIX_FMT_RGB24);
-       uint8_t *plane = video_input_frame.GetDataPlane(0);
-       const int linesize = video_input_frame.GetPlaneLinesize(0);
+       uint8_t *plane = stream.GetVideoPlane();
+       const int linesize = stream.GetVideoLineSize();
 
-       ffmpeg::Frame video_output_frame;
-       video_encoder.AllocateVideoFrame(video_output_frame);
-       ffmpeg::Packet video_packet;
+       int16_t *audio_plane = stream.GetAudioPlane();
+       const int audio_channels = stream.GetAudioChannels();
 
-       ffmpeg::Frame audio_input_frame;
-       audio_input_frame.AllocateAudio(audio_encoder.GetFrameSize(), AV_SAMPLE_FMT_S16, audio_encoder.GetChannelLayout());
-       int16_t *audio_plane = reinterpret_cast<int16_t *>(audio_input_frame.GetDataPlane(0));
-       const int audio_channels = audio_encoder.GetChannelLayout().nb_channels;
+       cairo::Surface surface(plane, linesize, CAIRO_FORMAT_ARGB32, WIDTH, HEIGHT);
+       cairo::Context context(surface.CreateContext());
 
-       ffmpeg::Frame audio_output_frame;
-       audio_encoder.AllocateAudioFrame(audio_output_frame);
-       ffmpeg::Packet audio_packet;
-
-       output.Open();
-       output.WriteHeader();
+       stream.Start();
 
        std::cout << std::endl;
-       const auto start = std::chrono::high_resolution_clock::now();
-
-       int64_t video_frame_counter = 0;
-       int64_t audio_frame_counter = 0;
 
        while (running) {
-               const int64_t target = av_rescale_q(video_frame_counter, video_encoder.GetTimeBase(), AVRational{1, 1000});
-               const auto now = std::chrono::high_resolution_clock::now();
-               const int64_t elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
+               const int64_t target = stream.VideoElapsedMS();
+               const int64_t elapsed = stream.TimeElapsedMS();
                const int64_t difference = target - elapsed;
-               if (video_frame_counter > 0 && difference < 0) {
+
+               stream.PrepareVideoFrame();
+
+               if (stream.GetVideoFrameCounter() > 0 && difference < 0) {
                        std::cout << (difference / 1000.0) << "s behind schedule, dropping frame" << std::endl;
-                       ++video_frame_counter;
-                       continue;
+               } else {
+                       RenderFrame(stream.GetVideoFrameCounter(), target, context);
+                       surface.Flush();
                }
 
-               video_input_frame.MakeWritable();
-               video_input_frame.SetSampleAspectRatio(AVRational{1, 1});
-               for (int y = 0; y < HEIGHT; ++y) {
-                       for (int x = 0; x < WIDTH; ++x) {
-                               int offset = y * linesize + x * 3;
-                               plane[offset] = (video_frame_counter + x) % 255;
-                               plane[offset + 1] = (video_frame_counter + 2 * x) % 255;
-                               plane[offset + 2] = (video_frame_counter + 3 * x) % 255;
-                       }
-               }
-               scaler.ScaleFrame(video_input_frame, video_output_frame);
-               video_output_frame.SetPresentationTimestamp(video_frame_counter);
-               video_encoder.SendFrame(video_output_frame);
-               ++video_frame_counter;
-               while (video_encoder.ReceivePacket(video_packet)) {
-                       video_packet.Rescale(video_encoder, video_stream);
-                       video_packet.SetStream(video_stream);
-                       output.WriteInterleavedPacket(video_packet);
-               }
+               stream.PushVideoFrame();
 
-               int64_t audio_ms = av_rescale_q(audio_frame_counter, audio_encoder.GetTimeBase(), AVRational{1, 1000});
-               while (audio_ms < target) {
-                       audio_input_frame.MakeWritable();
-                       for (int i = 0; i < audio_encoder.GetFrameSize(); ++i) {
+               while (stream.AudioElapsedMS() < target) {
+                       stream.PrepareAudioFrame();
+                       for (int i = 0; i < stream.GetAudioFrameSize(); ++i) {
                                for (int j = 0; j < audio_channels; ++j) {
                                        audio_plane[i * audio_channels + j] = 0;
                                }
                        }
-                       resampler.Convert(audio_encoder, audio_input_frame, audio_output_frame);
-                       audio_output_frame.SetPresentationTimestamp(audio_frame_counter);
-                       audio_encoder.SendFrame(audio_output_frame);
-                       while (audio_encoder.ReceivePacket(audio_packet)) {
-                               audio_packet.Rescale(audio_encoder, audio_stream);
-                               audio_packet.SetStream(audio_stream);
-                               output.WriteInterleavedPacket(audio_packet);
-                       }
-                       audio_frame_counter += audio_encoder.GetFrameSize();
-                       audio_ms = av_rescale_q(audio_frame_counter, audio_encoder.GetTimeBase(), AVRational{1, 1000});
+                       stream.PushAudioFrame();
                }
 
-               if (video_frame_counter % 60 == 59) {
+               if (stream.GetVideoFrameCounter() % 60 == 59) {
                        std::cout << "rendered: " << (target / 1000.0) << "s, elapsed: " << (elapsed / 1000.0) << "s, difference: " << (difference / 1000.0) << 's' << std::endl;
                }
                if (difference > 3000) {
@@ -169,20 +107,7 @@ int main(int argc, char**argv) {
 
        std::cout << std::endl;
 
-       video_encoder.Flush();
-       while (video_encoder.ReceivePacket(video_packet)) {
-               video_packet.Rescale(video_encoder, video_stream);
-               video_packet.SetStream(video_stream);
-               output.WriteInterleavedPacket(video_packet);
-       }
-
-       audio_encoder.Flush();
-       while (audio_encoder.ReceivePacket(audio_packet)) {
-               audio_packet.Rescale(audio_encoder, audio_stream);
-               audio_packet.SetStream(audio_stream);
-               output.WriteInterleavedPacket(audio_packet);
-       }
+       stream.Finish();
 
-       output.WriteTrailer();
        return 0;
 }