From 5aec7a66c696ea25e48e78fd01a64a452619b1a8 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sun, 22 Sep 2024 00:14:47 +0200 Subject: [PATCH 1/1] initial working version --- .gitignore | 4 + Makefile | 10 ++ src/ffmpeg/CodecContext.h | 198 +++++++++++++++++++++++++++++++++++++ src/ffmpeg/Encoder.h | 63 ++++++++++++ src/ffmpeg/Error.h | 30 ++++++ src/ffmpeg/FormatContext.h | 59 +++++++++++ src/ffmpeg/Frame.h | 155 +++++++++++++++++++++++++++++ src/ffmpeg/InputContext.h | 43 ++++++++ src/ffmpeg/OutputContext.h | 117 ++++++++++++++++++++++ src/ffmpeg/Packet.h | 78 +++++++++++++++ src/ffmpeg/Resampler.h | 86 ++++++++++++++++ src/ffmpeg/Scaler.h | 54 ++++++++++ src/ffmpeg/Stream.h | 48 +++++++++ src/ffmpeg/io.h | 18 ++++ src/main.cpp | 188 +++++++++++++++++++++++++++++++++++ 15 files changed, 1151 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 src/ffmpeg/CodecContext.h create mode 100644 src/ffmpeg/Encoder.h create mode 100644 src/ffmpeg/Error.h create mode 100644 src/ffmpeg/FormatContext.h create mode 100644 src/ffmpeg/Frame.h create mode 100644 src/ffmpeg/InputContext.h create mode 100644 src/ffmpeg/OutputContext.h create mode 100644 src/ffmpeg/Packet.h create mode 100644 src/ffmpeg/Resampler.h create mode 100644 src/ffmpeg/Scaler.h create mode 100644 src/ffmpeg/Stream.h create mode 100644 src/ffmpeg/io.h create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c38f59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.gdb_history +main +out.flv +test.mp4 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f363fb --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +CPP_SRCS = $(shell find src -name \*.cpp) +CPP_DEPS = $(shell find src -name \*.h) + +main: $(CPP_SRCS) $(CPP_DEPS) + clang++ -g $(shell pkg-config -cflags -libs libavformat libavcodec libavutil libswresample libswscale) $(CPP_SRCS) -o $@ + +run: main + ./main + +.PHONY: run diff --git a/src/ffmpeg/CodecContext.h b/src/ffmpeg/CodecContext.h new file mode 100644 index 0000000..b22b266 --- /dev/null +++ b/src/ffmpeg/CodecContext.h @@ -0,0 +1,198 @@ +#ifndef TEST_FFMPEG_CODECCONTEXT_H_ +#define TEST_FFMPEG_CODECCONTEXT_H_ + +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +#include "Frame.h" + +namespace ffmpeg { + +class CodecContext { + +public: + CodecContext(const AVCodec *codec): codec(codec) { + if (!codec) { + throw std::runtime_error("unable to find codec"); + } + ctx = avcodec_alloc_context3(codec); + if (!ctx) { + throw std::runtime_error("failed to allocate context"); + } + } + ~CodecContext() { + avcodec_free_context(&ctx); + } + void Swap(CodecContext &other) { + std::swap(ctx, other.ctx); + std::swap(codec, other.codec); + } + + CodecContext(const CodecContext &) = delete; + CodecContext &operator =(const CodecContext &) = delete; + +public: + void AllocateAudioFrame(Frame &frame) { + frame.AllocateAudio(ctx->frame_size, ctx->sample_fmt, ctx->ch_layout); + } + + void AllocateVideoFrame(Frame &frame) { + frame.AllocateImage(ctx->width, ctx->height, ctx->pix_fmt); + } + + AVChannelLayout GetChannelLayout() const { + return ctx->ch_layout; + } + + AVRational GetFrameRate() const { + return ctx->framerate; + } + + int GetFrameSize() const { + return ctx->frame_size; + } + + AVPixelFormat GetPixelFormat() const { + return ctx->pix_fmt; + } + + AVSampleFormat GetSampleFormat() const { + return ctx->sample_fmt; + } + + int GetSampleRate() const { + return ctx->sample_rate; + } + + AVRational GetTimeBase() const { + return ctx->time_base; + } + + void InferPixelFormat() { + ctx->pix_fmt = codec->pix_fmts[0]; + } + + void InferSampleFormat() { + ctx->sample_fmt = codec->sample_fmts[0]; + } + + void Open() { + if (avcodec_open2(ctx, codec, nullptr) != 0) { + throw std::runtime_error("failed to open audio codec"); + } + } + + void SetBitRate(int64_t rate) { + ctx->bit_rate = rate; + } + + void SetBufSize(int64_t size) { + ctx->rc_buffer_size = size; + } + + void SetMaxBitRate(int64_t rate) { + ctx->rc_max_rate = rate; + } + + void SetMinBitRate(int64_t rate) { + ctx->rc_min_rate = rate; + } + + void SetDefaultChannelLayout(int num_channels) { + av_channel_layout_default(&ctx->ch_layout, num_channels); + } + + void SetFieldOrder(AVFieldOrder order) { + ctx->field_order = order; + } + + void SetFrameRate(AVRational rate) { + ctx->framerate = rate; + ctx->time_base = av_inv_q(rate); + } + + void SetGlobalHeader() { + ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + void SetGopSize(int size) { + ctx->gop_size = size; + } + + void SetOption(const char *name, const char *value) { + int res = av_opt_set(ctx->priv_data, name, value, 0); + if (res != 0) { + throw Error("failed to set codec option", res); + } + } + + void SetPixelFormat(AVPixelFormat format) { + ctx->pix_fmt = format; + } + + void SetSampleAspectRatio(AVRational ratio) { + ctx->sample_aspect_ratio = ratio; + } + + void SetSampleFormat(AVSampleFormat fmt) { + const AVSampleFormat *p = codec->sample_fmts; + while (*p != AV_SAMPLE_FMT_NONE) { + if (*p == fmt) { + ctx->sample_fmt = fmt; + return; + } + ++p; + } + throw std::runtime_error("unsupported sample format"); + } + + void SetSampleRate(int rate) { + ctx->sample_rate = rate; + ctx->time_base = AVRational{1, rate}; + } + + void SetSize(int width, int height) { + ctx->width = width; + ctx->height = height; + } + + bool SupportsVariableFrameSize() const { + return codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE; + } + + void WriteParameters(AVCodecParameters ¶ms) const { + int res = avcodec_parameters_from_context(¶ms, ctx); + if (res != 0) { + throw Error("failed to copy codec params", res); + } + } + +protected: + ::AVCodecContext *ctx; + const ::AVCodec *codec; + +}; + +} + +namespace std { +inline void swap( + ffmpeg::CodecContext &lhs, + ffmpeg::CodecContext &rhs) { + lhs.Swap(rhs); +} +} + +#endif diff --git a/src/ffmpeg/Encoder.h b/src/ffmpeg/Encoder.h new file mode 100644 index 0000000..d9a6d1d --- /dev/null +++ b/src/ffmpeg/Encoder.h @@ -0,0 +1,63 @@ +#ifndef TEST_FFMPEG_ENCODER_H_ +#define TEST_FFMPEG_ENCODER_H_ + +#include + +extern "C" { +#include +#include +#include +#include +} + +#include "CodecContext.h" +#include "Error.h" +#include "Frame.h" +#include "Packet.h" + +namespace ffmpeg { + +class Encoder: public CodecContext { + +public: + Encoder(AVCodecID id): CodecContext(avcodec_find_encoder(id)) { + + } + Encoder(const char *name): CodecContext(avcodec_find_encoder_by_name(name)) { + + } + +public: + void SendFrame(const Frame &frame) { + int res = avcodec_send_frame(ctx, frame.GetFrame()); + if (res != 0) { + throw Error("failed to send frame", res); + } + } + + void Flush() { + int res = avcodec_send_frame(ctx, nullptr); + if (res != 0) { + throw Error("failed to flush encoder", res); + } + } + + bool ReceivePacket(Packet &packet) { + int res = avcodec_receive_packet(ctx, packet.GetPacket()); + if (res == AVERROR(EAGAIN)) { + return false; + } + if (res == AVERROR_EOF) { + return false; + } + if (res != 0) { + throw Error("failed to receive packet", res); + } + return true; + } + +}; + +} + +#endif diff --git a/src/ffmpeg/Error.h b/src/ffmpeg/Error.h new file mode 100644 index 0000000..eb66425 --- /dev/null +++ b/src/ffmpeg/Error.h @@ -0,0 +1,30 @@ +#ifndef TEST_FFMPEG_ERROR_H_ +#define TEST_FFMPEG_ERROR_H_ + +#include +#include +#include + +namespace { +std::string make_message(const std::string &msg, int code) { + char buf[128] = {0}; + av_strerror(code, buf, sizeof(buf)); + return msg + ": " + std::string(buf); +} +} + +namespace ffmpeg { + +class Error: public std::runtime_error { + +public: + Error(const std::string &msg, int code): std::runtime_error(make_message(msg, code)) { + } + ~Error() { + } + +}; + +} + +#endif diff --git a/src/ffmpeg/FormatContext.h b/src/ffmpeg/FormatContext.h new file mode 100644 index 0000000..d15ddc0 --- /dev/null +++ b/src/ffmpeg/FormatContext.h @@ -0,0 +1,59 @@ +#ifndef TEST_FFMPEG_FORMATCONTEXT_H_ +#define TEST_FFMPEG_FORMATCONTEXT_H_ + +#include "CodecContext.h" +#include +#include + +extern "C" { +#include +} + +namespace ffmpeg { + +class FormatContext { + +public: + explicit FormatContext(AVFormatContext *ctx): ctx(ctx) { + if (!ctx) { + throw std::runtime_error("failed to allocate context"); + } + } + ~FormatContext() { + avformat_free_context(ctx); + } + void Swap(FormatContext &other) { + std::swap(ctx, other.ctx); + } + + FormatContext(const FormatContext &) = delete; + FormatContext &operator =(const FormatContext &) = delete; + +public: + void Dump(int index, const char *url, int is_output) { + av_dump_format(ctx, index, url, is_output); + } + + void SetOption(const char *name, const char *value) { + int res = av_opt_set(ctx->priv_data, name, value, 0); + if (res != 0) { + throw Error("failed to set format option", res); + } + } + +protected: + AVFormatContext *ctx; + +}; + +} + +namespace std { +inline void swap( + ffmpeg::FormatContext &lhs, + ffmpeg::FormatContext &rhs) { + lhs.Swap(rhs); +} +} + +#endif diff --git a/src/ffmpeg/Frame.h b/src/ffmpeg/Frame.h new file mode 100644 index 0000000..efc03d8 --- /dev/null +++ b/src/ffmpeg/Frame.h @@ -0,0 +1,155 @@ +#ifndef TEST_FFMPEG_FRAME_H_ +#define TEST_FFMPEG_FRAME_H_ + +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +} + +#include "Error.h" + +namespace ffmpeg { + +class Frame { + +public: + Frame(): frame(av_frame_alloc()) { + if (!frame) { + throw std::runtime_error("failed to allocate frame"); + } + } + ~Frame() { + av_frame_free(&frame); + } + Frame(const Frame &other) = delete; + Frame &operator =(const Frame &other) = delete; + +public: + void AllocateAudio(int frame_size, AVSampleFormat fmt, const AVChannelLayout &layout) { + if (buffer_allocated) { + std::cout << "freeing data" << std::endl; + av_freep(frame->data); + buffer_allocated = false; + } + SetSamples(frame_size); + SetSampleFormat(fmt); + SetChannelLayout(layout); + int res = av_frame_get_buffer(frame, 0); + if (res < 0) { + throw Error("failed to allocate audio buffer", res); + } + buffer_allocated = true; + } + + void AllocateImage(int width, int height, AVPixelFormat format) { + if (buffer_allocated) { + av_freep(frame->data); + buffer_allocated = false; + } + frame->width = width; + frame->height = height; + frame->format = format; + int res = av_frame_get_buffer(frame, 0); + if (res < 0) { + throw Error("failed to allocate image buffer", res); + } + buffer_allocated = true; + } + + void MakeWritable() { + int res = av_frame_make_writable(frame); + if (res != 0) { + throw Error("cannot make frame writable", res); + } + } + + uint8_t **GetData() { + return frame->data; + } + + const uint8_t **GetData() const { + return const_cast(frame->data); + } + + uint8_t *GetDataPlane(unsigned int num) { + if (num >= 8) { + throw std::runtime_error("plane index out of bounds"); + } + if (frame->data[num] == nullptr) { + throw std::runtime_error("plane does not exist"); + } + return frame->data[num]; + } + + const uint8_t *GetDataPlane(unsigned int num) const { + if (num >= 8) { + throw std::runtime_error("plane index out of bounds"); + } + if (frame->data[num] == nullptr) { + throw std::runtime_error("plane does not exist"); + } + return frame->data[num]; + } + + + const int GetPlaneLinesize(unsigned int num) { + if (num >= 8) { + throw std::runtime_error("plane index out of bounds"); + } + return frame->linesize[num]; + } + + int64_t GetPresentationTimestamp() const { + return frame->pts; + } + + int GetSamples() const { + return frame->nb_samples; + } + + void SetChannelLayout(AVChannelLayout layout) { + frame->ch_layout = layout; + } + + void SetPresentationTimestamp(int64_t ts) { + frame->pts = ts; + } + + void SetSamples(int num) { + frame->nb_samples = num; + } + + void SetSampleAspectRatio(AVRational ratio) { + frame->sample_aspect_ratio = ratio; + } + + void SetSampleFormat(int fmt) { + frame->format = fmt; + } + + AVFrame *GetFrame() { + return frame; + } + + const AVFrame *GetFrame() const { + return frame; + } + +private: + AVFrame *frame; + bool buffer_allocated = false; + +}; + +} + +#endif diff --git a/src/ffmpeg/InputContext.h b/src/ffmpeg/InputContext.h new file mode 100644 index 0000000..7e821b7 --- /dev/null +++ b/src/ffmpeg/InputContext.h @@ -0,0 +1,43 @@ +#ifndef TEST_FFMPEG_INPUTCONTEXT_H_ +#define TEST_FFMPEG_INPUTCONTEXT_H_ + +#include + +extern "C" { +#include +} + +#include "FormatContext.h" + +namespace ffmpeg { + +class InputContext: public FormatContext { + + public: + explicit InputContext(const char *url) + : FormatContext(avformat_alloc_context()) + , url(url) { + if (avformat_open_input(&ctx, url, nullptr, nullptr) != 0) { + avformat_free_context(ctx); + throw std::runtime_error("failed to open input file"); + } + } + ~InputContext() { + } + + InputContext(const InputContext &) = delete; + InputContext &operator =(const InputContext &) = delete; + +public: + void Dump(int index) { + FormatContext::Dump(index, url, 0); + } + +private: + const char *url; + +}; + +} + +#endif diff --git a/src/ffmpeg/OutputContext.h b/src/ffmpeg/OutputContext.h new file mode 100644 index 0000000..0dab87a --- /dev/null +++ b/src/ffmpeg/OutputContext.h @@ -0,0 +1,117 @@ +#ifndef TEST_FFMPEG_OUTPUTCONTEXT_H_ +#define TEST_FFMPEG_OUTPUTCONTEXT_H_ + +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +#include "FormatContext.h" +#include "Packet.h" +#include "Stream.h" + +namespace { +AVFormatContext *alloc_context(const char *url, const char *format) { + AVFormatContext *ctx = nullptr; + avformat_alloc_output_context2(&ctx, nullptr, format, url); + return ctx; +} +} + +namespace ffmpeg { + +class OutputContext: public FormatContext { + + public: + OutputContext(const char *url, const char *format) + : FormatContext(alloc_context(url, format)) + , url(url) { + } + ~OutputContext() { + } + + OutputContext(const OutputContext &) = delete; + OutputContext &operator =(const OutputContext &) = delete; + +public: + Stream CreateAudioStream(CodecContext &codec) { + AVStream *s = avformat_new_stream(ctx, nullptr); + if (!s) { + throw std::runtime_error("failed to allocate stream"); + } + s->id = ctx->nb_streams - 1; + s->time_base = AVRational{1, codec.GetSampleRate()}; + codec.WriteParameters(*s->codecpar); + return std::move(Stream(s)); + } + + Stream CreateVideoStream(CodecContext &codec) { + AVStream *s = avformat_new_stream(ctx, nullptr); + if (!s) { + throw std::runtime_error("failed to allocate stream"); + } + s->id = ctx->nb_streams - 1; + s->time_base = codec.GetTimeBase(); + s->avg_frame_rate = codec.GetFrameRate(); + codec.WriteParameters(*s->codecpar); + return std::move(Stream(s)); + } + + void Dump(int index) { + FormatContext::Dump(index, url, 1); + } + + void Open() { + if ((ctx->oformat->flags & AVFMT_NOFILE)) { + return; + } + int res = avio_open(&ctx->pb, url, AVIO_FLAG_WRITE); + if (res != 0) { + throw Error("unable to open output file", res); + } + } + + bool RequiresGlobalHeader() const { + return ctx->oformat->flags & AVFMT_GLOBALHEADER; + } + + void WriteHeader() { + int res = avformat_write_header(ctx, nullptr); + if (res != 0) { + throw Error("failed to write header", res); + } + } + + void WriteInterleavedPacket(Packet &packet) { + int res = av_interleaved_write_frame(ctx, packet.GetPacket()); + if (res != 0) { + throw Error("failed to write packet to stream", res); + } + } + + void WritePacket(Packet &packet) { + int res = av_write_frame(ctx, packet.GetPacket()); + if (res != 0) { + throw Error("failed to write packet to stream", res); + } + } + + void WriteTrailer() { + if (av_write_trailer(ctx) != 0) { + throw std::runtime_error("failed to write trailer"); + } + } + +private: + const char *url; + +}; + +} + +#endif diff --git a/src/ffmpeg/Packet.h b/src/ffmpeg/Packet.h new file mode 100644 index 0000000..ee439e3 --- /dev/null +++ b/src/ffmpeg/Packet.h @@ -0,0 +1,78 @@ +#ifndef TEST_FFMPEG_PACKET_H_ +#define TEST_FFMPEG_PACKET_H_ + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +#include "io.h" +#include "Stream.h" + +namespace ffmpeg { + +class Packet { + +public: + Packet(): packet(av_packet_alloc()) { + if (!packet) { + throw std::runtime_error("failed to allocate packet"); + } + } + ~Packet() { + av_packet_free(&packet); + } + Packet(const Packet &other): packet(av_packet_clone(other.packet)) { + if (!packet) { + throw std::runtime_error("failed to allocate packet"); + } + } + Packet &operator =(const Packet &) = delete; + +public: + AVPacket *GetPacket() { + return packet; + } + + const AVPacket *GetPacket() const { + return packet; + } + + void Rescale(const CodecContext &context, const Stream &stream) { + av_packet_rescale_ts(packet, context.GetTimeBase(), stream.GetTimeBase()); + } + + int64_t GetDecompressionTimestamp() const { + return packet->dts; + } + + int64_t GetDuration() const { + return packet->duration; + } + + int64_t GetPresentationTimestamp() const { + return packet->pts; + } + + void SetStream(const Stream &stream) { + packet->stream_index = stream.GetIndex(); + } + + int GetStreamIndex() const { + return packet->stream_index; + } + +private: + AVPacket *packet; + +}; + +} + +#endif diff --git a/src/ffmpeg/Resampler.h b/src/ffmpeg/Resampler.h new file mode 100644 index 0000000..b38c48f --- /dev/null +++ b/src/ffmpeg/Resampler.h @@ -0,0 +1,86 @@ +#ifndef TEST_FFMPEG_RESAMPLER_H_ +#define TEST_FFMPEG_RESAMPLER_H_ + +#include "CodecContext.h" +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +#include "Error.h" +#include "Frame.h" + +namespace ffmpeg { + +class Resampler { + +public: + Resampler(): ctx(swr_alloc()) { + if (!ctx) { + throw std::runtime_error("failed to allocate resampler context"); + } + } + Resampler(int src_chans, int src_rate, AVSampleFormat src_fmt, int dst_chans, int dst_rate, AVSampleFormat dst_fmt) + : ctx(swr_alloc()) { + if (!ctx) { + throw std::runtime_error("failed to allocate resampler context"); + } + SetOpt("in_channel_count", src_chans); + SetOpt("in_sample_rate", src_rate); + SetOpt("in_sample_fmt", src_fmt); + SetOpt("out_channel_count", dst_chans); + SetOpt("out_sample_rate", dst_rate); + SetOpt("out_sample_fmt", dst_fmt); + Init(); + } + ~Resampler() { + swr_free(&ctx); + } + + Resampler(const Resampler &) = delete; + Resampler &operator =(const Resampler &) = delete; + +public: + void Convert(const CodecContext &codec, const Frame &src, Frame &dst) { + int64_t from = swr_get_delay(ctx, codec.GetSampleRate()) + src.GetSamples(); + int64_t nb_samples = av_rescale_rnd(from, codec.GetSampleRate(), codec.GetSampleRate(), AV_ROUND_UP); + int res = swr_convert(ctx, dst.GetData(), nb_samples, src.GetData(), src.GetSamples()); + if (res < 0) { + throw Error("failed to resample", res); + } + } + + void Init() { + int res = swr_init(ctx); + if (res != 0) { + throw Error("failed to initialize resampler", res); + } + } + + void SetOpt(const char *name, int value) { + int res = av_opt_set_int(ctx, name, value, 0); + if (res != 0) { + throw Error("failed to set option", res); + } + } + + void SetOpt(const char *name, AVSampleFormat value) { + int res = av_opt_set_sample_fmt(ctx, name, value, 0); + if (res != 0) { + throw Error("failed to set option", res); + } + } + +private: + SwrContext *ctx; + +}; + +} + +#endif diff --git a/src/ffmpeg/Scaler.h b/src/ffmpeg/Scaler.h new file mode 100644 index 0000000..4adbab6 --- /dev/null +++ b/src/ffmpeg/Scaler.h @@ -0,0 +1,54 @@ +#ifndef TEST_FFMPEG_SCALER_H_ +#define TEST_FFMPEG_SCALER_H_ + +#include "Error.h" +#include +#include +#include + +extern "C" { +#include +#include +} + +#include "Frame.h" + +namespace ffmpeg { + +class Scaler { + +public: + Scaler(): ctx(sws_alloc_context()) { + if (!ctx) { + throw std::runtime_error("failed to allocate scaler context"); + } + } + Scaler(int srcW, int srcH, AVPixelFormat srcFormat, int dstW, int dstH, AVPixelFormat dstFormat) + : ctx(sws_getContext(srcW, srcH, srcFormat, dstW, dstH, dstFormat, 0, nullptr, nullptr, nullptr)) { + if (!ctx) { + throw std::runtime_error("failed to allocate scaler context"); + } + } + ~Scaler() { + sws_freeContext(ctx); + } + + Scaler(const Scaler &) = delete; + Scaler &operator =(const Scaler &) = delete; + +public: + void ScaleFrame(const Frame &src, Frame &dst) { + int res = sws_scale_frame(ctx, dst.GetFrame(), src.GetFrame()); + if (res < 0) { + throw Error("failed to scale frame", res); + } + } + +private: + SwsContext *ctx; + +}; + +} + +#endif diff --git a/src/ffmpeg/Stream.h b/src/ffmpeg/Stream.h new file mode 100644 index 0000000..420c372 --- /dev/null +++ b/src/ffmpeg/Stream.h @@ -0,0 +1,48 @@ +#ifndef TEST_FFMPEG_STREAM_H_ +#define TEST_FFMPEG_STREAM_H_ + +#include + +extern "C" { +#include +#include +} + +#include "CodecContext.h" +#include "Error.h" + +namespace ffmpeg { + +class Stream { + +public: + explicit Stream(AVStream *s): s(s) { + if (!s) { + throw std::runtime_error("failed to allocate stream"); + } + } + ~Stream() { + } + Stream(Stream &&other): s(other.s) { + } + + Stream(const Stream &) = delete; + Stream &operator =(const Stream &) = delete; + +public: + int GetIndex() const { + return s->index; + } + + AVRational GetTimeBase() const { + return s->time_base; + } + +private: + AVStream *s; + +}; + +} + +#endif diff --git a/src/ffmpeg/io.h b/src/ffmpeg/io.h new file mode 100644 index 0000000..925f0c3 --- /dev/null +++ b/src/ffmpeg/io.h @@ -0,0 +1,18 @@ +#ifndef TEST_FFMPEG_IO_H_ +#define TEST_FFMPEG_IO_H_ + +#include + +extern "C" { +#include +} + +namespace std { + +inline std::ostream &operator <<(std::ostream &out, AVRational r) { + return out << r.num << '/' << r.den; +} + +} + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..b4f4489 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +#include "ffmpeg/Encoder.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; +} + +} + + +int main(int argc, char**argv) { + const int WIDTH = 1280; + const int HEIGHT = 720; + + int res = avformat_network_init(); + if (res != 0) { + throw ffmpeg::Error("network init failed", res); + } + + 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); + + ffmpeg::Frame video_output_frame; + video_encoder.AllocateVideoFrame(video_output_frame); + ffmpeg::Packet video_packet; + + 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; + + ffmpeg::Frame audio_output_frame; + audio_encoder.AllocateAudioFrame(audio_output_frame); + ffmpeg::Packet audio_packet; + + output.Open(); + output.WriteHeader(); + + 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 difference = target - elapsed; + if (video_frame_counter > 0 && difference < 0) { + std::cout << (difference / 1000.0) << "s behind schedule, dropping frame" << std::endl; + ++video_frame_counter; + continue; + } + + 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); + } + + 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) { + 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}); + } + + if (video_frame_counter % 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::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + 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); + } + + output.WriteTrailer(); + return 0; +} -- 2.39.2