.gdb_history
+compile_flags.txt
main
out.flv
test.mp4
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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
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];
}
#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};
}
- const int GetPlaneLinesize(unsigned int num) {
+ int GetPlaneLinesize(unsigned int num) const {
if (num >= 8) {
throw std::runtime_error("plane index out of bounds");
}
#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");
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 {
+#include "cairo.h"
+#include "cairo/Context.h"
#include <chrono>
#include <csignal>
#include <cstdint>
#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) {
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;
}