From: Daniel Karbach Date: Sun, 22 Sep 2024 19:29:47 +0000 (+0200) Subject: basic cairo X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=5a638955387cc3768a5faf06238963bddd14530b;p=ffmpeg-test.git basic cairo --- diff --git a/.gitignore b/.gitignore index 2c38f59..b499675 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gdb_history +compile_flags.txt main out.flv test.mp4 diff --git a/Makefile b/Makefile index 5f363fb..48f4d5c 100644 --- 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 index 0000000..5da2a54 --- /dev/null +++ b/src/app/Stream.h @@ -0,0 +1,182 @@ +#ifndef TEST_APP_STREAM_H_ +#define TEST_APP_STREAM_H_ + +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +#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(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(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 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 index 0000000..92cd24a --- /dev/null +++ b/src/cairo/Context.h @@ -0,0 +1,95 @@ +#ifndef TEST_CAIRO_CONTEXT_H_ +#define TEST_CAIRO_CONTEXT_H_ + +#include +#include +#include + +#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 index 0000000..a0b869b --- /dev/null +++ b/src/cairo/Error.h @@ -0,0 +1,29 @@ +#ifndef TEST_CAIRO_ERROR_H_ +#define TEST_CAIRO_ERROR_H_ + +#include +#include + +#include + +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 index 0000000..8e3a15b --- /dev/null +++ b/src/cairo/Surface.h @@ -0,0 +1,57 @@ +#ifndef TEST_CAIRO_SURFACE_H_ +#define TEST_CAIRO_SURFACE_H_ + +#include + +#include + +#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 diff --git a/src/ffmpeg/CodecContext.h b/src/ffmpeg/CodecContext.h index b22b266..0a8ceec 100644 --- a/src/ffmpeg/CodecContext.h +++ b/src/ffmpeg/CodecContext.h @@ -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]; } diff --git a/src/ffmpeg/Error.h b/src/ffmpeg/Error.h index eb66425..bdcbdce 100644 --- a/src/ffmpeg/Error.h +++ b/src/ffmpeg/Error.h @@ -1,10 +1,13 @@ #ifndef TEST_FFMPEG_ERROR_H_ #define TEST_FFMPEG_ERROR_H_ -#include #include #include +extern "C" { +#include +} + namespace { std::string make_message(const std::string &msg, int code) { char buf[128] = {0}; diff --git a/src/ffmpeg/Frame.h b/src/ffmpeg/Frame.h index efc03d8..836d07d 100644 --- a/src/ffmpeg/Frame.h +++ b/src/ffmpeg/Frame.h @@ -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"); } diff --git a/src/ffmpeg/Stream.h b/src/ffmpeg/Stream.h index 420c372..88ec5d7 100644 --- a/src/ffmpeg/Stream.h +++ b/src/ffmpeg/Stream.h @@ -8,14 +8,13 @@ extern "C" { #include } -#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 { diff --git a/src/main.cpp b/src/main.cpp index b4f4489..a77f4d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,5 @@ +#include "cairo.h" +#include "cairo/Context.h" #include #include #include @@ -17,149 +19,85 @@ extern "C" { #include } -#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(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(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; }