]> git.localhorst.tv Git - ffmpeg-test.git/commitdiff
initial working version
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 21 Sep 2024 22:14:47 +0000 (00:14 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 21 Sep 2024 22:15:19 +0000 (00:15 +0200)
15 files changed:
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
src/ffmpeg/CodecContext.h [new file with mode: 0644]
src/ffmpeg/Encoder.h [new file with mode: 0644]
src/ffmpeg/Error.h [new file with mode: 0644]
src/ffmpeg/FormatContext.h [new file with mode: 0644]
src/ffmpeg/Frame.h [new file with mode: 0644]
src/ffmpeg/InputContext.h [new file with mode: 0644]
src/ffmpeg/OutputContext.h [new file with mode: 0644]
src/ffmpeg/Packet.h [new file with mode: 0644]
src/ffmpeg/Resampler.h [new file with mode: 0644]
src/ffmpeg/Scaler.h [new file with mode: 0644]
src/ffmpeg/Stream.h [new file with mode: 0644]
src/ffmpeg/io.h [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2c38f59
--- /dev/null
@@ -0,0 +1,4 @@
+.gdb_history
+main
+out.flv
+test.mp4
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..b22b266
--- /dev/null
@@ -0,0 +1,198 @@
+#ifndef TEST_FFMPEG_CODECCONTEXT_H_
+#define TEST_FFMPEG_CODECCONTEXT_H_
+
+#include <cstdint>
+#include <stdexcept>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavcodec/codec.h>
+#include <libavcodec/codec_par.h>
+#include <libavutil/channel_layout.h>
+#include <libavutil/imgutils.h>
+#include <libavutil/opt.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/rational.h>
+#include <libavutil/samplefmt.h>
+}
+
+#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 &params) const {
+               int res = avcodec_parameters_from_context(&params, 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 (file)
index 0000000..d9a6d1d
--- /dev/null
@@ -0,0 +1,63 @@
+#ifndef TEST_FFMPEG_ENCODER_H_
+#define TEST_FFMPEG_ENCODER_H_
+
+#include <cerrno>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavcodec/codec.h>
+#include <libavcodec/codec_id.h>
+#include <libavutil/error.h>
+}
+
+#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 (file)
index 0000000..eb66425
--- /dev/null
@@ -0,0 +1,30 @@
+#ifndef TEST_FFMPEG_ERROR_H_
+#define TEST_FFMPEG_ERROR_H_
+
+#include <libavutil/error.h>
+#include <stdexcept>
+#include <string>
+
+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 (file)
index 0000000..d15ddc0
--- /dev/null
@@ -0,0 +1,59 @@
+#ifndef TEST_FFMPEG_FORMATCONTEXT_H_
+#define TEST_FFMPEG_FORMATCONTEXT_H_
+
+#include "CodecContext.h"
+#include <stdexcept>
+#include <utility>
+
+extern "C" {
+#include <libavformat/avformat.h>
+}
+
+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 (file)
index 0000000..efc03d8
--- /dev/null
@@ -0,0 +1,155 @@
+#ifndef TEST_FFMPEG_FRAME_H_
+#define TEST_FFMPEG_FRAME_H_
+
+#include <cstdint>
+#include <iostream>
+#include <stdexcept>
+
+extern "C" {
+#include <libavutil/channel_layout.h>
+#include <libavutil/frame.h>
+#include <libavutil/imgutils.h>
+#include <libavutil/mem.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/rational.h>
+#include <libavutil/samplefmt.h>
+}
+
+#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<const uint8_t **>(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 (file)
index 0000000..7e821b7
--- /dev/null
@@ -0,0 +1,43 @@
+#ifndef TEST_FFMPEG_INPUTCONTEXT_H_
+#define TEST_FFMPEG_INPUTCONTEXT_H_
+
+#include <stdexcept>
+
+extern "C" {
+#include <libavformat/avformat.h>
+}
+
+#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 (file)
index 0000000..0dab87a
--- /dev/null
@@ -0,0 +1,117 @@
+#ifndef TEST_FFMPEG_OUTPUTCONTEXT_H_
+#define TEST_FFMPEG_OUTPUTCONTEXT_H_
+
+#include <alloca.h>
+#include <libavutil/rational.h>
+#include <stdexcept>
+#include <utility>
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+}
+
+#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 (file)
index 0000000..ee439e3
--- /dev/null
@@ -0,0 +1,78 @@
+#ifndef TEST_FFMPEG_PACKET_H_
+#define TEST_FFMPEG_PACKET_H_
+
+#include <cstdint>
+#include <iostream>
+#include <libavutil/avutil.h>
+#include <libavutil/mathematics.h>
+#include <stdexcept>
+
+extern "C" {
+#include <libavcodec/packet.h>
+#include <libavutil/frame.h>
+}
+
+#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 (file)
index 0000000..b38c48f
--- /dev/null
@@ -0,0 +1,86 @@
+#ifndef TEST_FFMPEG_RESAMPLER_H_
+#define TEST_FFMPEG_RESAMPLER_H_
+
+#include "CodecContext.h"
+#include <cstdint>
+#include <libavutil/opt.h>
+#include <libavutil/samplefmt.h>
+#include <stdexcept>
+
+extern "C" {
+#include <libavutil/mathematics.h>
+#include <libswresample/swresample.h>
+}
+
+#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 (file)
index 0000000..4adbab6
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef TEST_FFMPEG_SCALER_H_
+#define TEST_FFMPEG_SCALER_H_
+
+#include "Error.h"
+#include <iostream>
+#include <libavutil/error.h>
+#include <stdexcept>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+#include <libswscale/swscale.h>
+}
+
+#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 (file)
index 0000000..420c372
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef TEST_FFMPEG_STREAM_H_
+#define TEST_FFMPEG_STREAM_H_
+
+#include <stdexcept>
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavutil/rational.h>
+}
+
+#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 (file)
index 0000000..925f0c3
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef TEST_FFMPEG_IO_H_
+#define TEST_FFMPEG_IO_H_
+
+#include <ostream>
+
+extern "C" {
+#include <libavutil/rational.h>
+}
+
+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 (file)
index 0000000..b4f4489
--- /dev/null
@@ -0,0 +1,188 @@
+#include <chrono>
+#include <csignal>
+#include <cstdint>
+#include <iostream>
+#include <string>
+#include <thread>
+
+extern "C" {
+#include <libavcodec/codec_id.h>
+#include <libavcodec/codec_par.h>
+#include <libavformat/avformat.h>
+#include <libavutil/buffer.h>
+#include <libavutil/mathematics.h>
+#include <libavutil/pixfmt.h>
+#include <libavutil/rational.h>
+#include <libavutil/samplefmt.h>
+#include <libavutil/timestamp.h>
+}
+
+#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<int16_t *>(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<std::chrono::milliseconds>(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;
+}