Last active
February 12, 2025 07:41
-
-
Save TekuConcept/ff33fad5d2141eaf62e8cbd4e26def49 to your computer and use it in GitHub Desktop.
FFmpeg stream forwarding in C++
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdint> | |
#include <string> | |
#include <memory> | |
extern "C" { | |
#include <libavcodec/avcodec.h> | |
#include <libavformat/avformat.h> | |
#include <libavutil/imgutils.h> | |
#include <libavutil/samplefmt.h> | |
#include <libavutil/timestamp.h> | |
#include <libavutil/opt.h> | |
#include <libswscale/swscale.h> | |
#include <libswresample/swresample.h> | |
#include <sys/time.h> | |
} | |
struct timebase_t { | |
int numerator; | |
int denominator; | |
}; | |
enum class media_type_t { | |
VIDEO, | |
AUDIO | |
}; | |
struct stream_desc_t { | |
int64_t bit_rate; | |
int width; | |
int height; | |
int channels; | |
int sample_rate; | |
timebase_t timebase; | |
media_type_t media_type; | |
std::string codec_name; | |
}; | |
enum class packet_type_t { UNKNOWN, VIDEO, AUDIO }; | |
class ffmpeg_input { | |
public: | |
ffmpeg_input(std::string __url); | |
~ffmpeg_input(); | |
bool read(AVPacket* packet, packet_type_t* type); | |
std::shared_ptr<stream_desc_t> video_desc() const; | |
std::shared_ptr<stream_desc_t> audio_desc() const; | |
private: | |
std::string m_url; | |
AVFormatContext* m_format_context; | |
AVFrame* m_frame; | |
AVPacket m_packet; | |
int m_video_stream_id; | |
int m_audio_stream_id; | |
AVStream* m_video_stream; | |
AVStream* m_audio_stream; | |
bool _M_open(const std::string& __source); | |
bool _M_open_input_stream(const std::string& __source); | |
inline bool _M_open_media_context(); | |
int _M_open_codec_context( | |
int* __stream_id, | |
AVFormatContext* __format_context, | |
AVStream** __stream, | |
AVMediaType __type); | |
std::shared_ptr<stream_desc_t> _M_stream2desc(int __index, bool __audio) const; | |
}; | |
class ffmpeg_output { | |
public: | |
ffmpeg_output( | |
std::string video_url, | |
std::shared_ptr<stream_desc_t> video_desc, | |
std::string audio_url, | |
std::shared_ptr<stream_desc_t> audio_desc); | |
~ffmpeg_output(); | |
bool write_video(AVPacket* packet); | |
bool write_audio(AVPacket* packet); | |
private: | |
std::string m_video_url; | |
std::string m_audio_url; | |
AVOutputFormat* m_format; | |
AVFormatContext* m_video_context; | |
AVFormatContext* m_audio_context; | |
AVStream* m_video_stream; | |
AVStream* m_audio_stream; | |
bool _M_open_video( | |
std::string __url, | |
std::shared_ptr<stream_desc_t> __desc, | |
AVFormatContext** __context, | |
AVStream** __stream); | |
bool _M_open_audio( | |
std::string __url, | |
std::shared_ptr<stream_desc_t> __desc, | |
AVFormatContext** __context, | |
AVStream** __stream); | |
}; | |
int main(int argc, char** argv) { | |
try { | |
AVPacket packet; | |
packet_type_t packet_type; | |
ffmpeg_input input(argv[1]); | |
ffmpeg_output output( | |
"rtp://localhost:5008/", input.video_desc(), | |
"rtp://localhost:5006/", input.audio_desc()); | |
while (input.read(&packet, &packet_type)) { | |
AVPacket out_packet; | |
av_init_packet(&out_packet); | |
out_packet.pts = packet.pts; | |
out_packet.flags = packet.flags; | |
out_packet.data = packet.data; | |
out_packet.size = packet.size; | |
switch (packet_type) { | |
case packet_type_t::VIDEO: output.write_video(&out_packet); break; | |
case packet_type_t::AUDIO: output.write_audio(&out_packet); break; | |
default: av_packet_unref(&out_packet); break; | |
} | |
} | |
} catch (std::runtime_error& e) { | |
std::cerr << e.what() << std::endl; | |
} | |
} | |
// ================================== | |
// ---------- ffmpeg_input ---------- | |
// ================================== | |
ffmpeg_input::ffmpeg_input(std::string __url) | |
: m_url(__url), | |
m_format_context(nullptr), | |
m_frame(nullptr), | |
m_video_stream_id(-1), | |
m_video_stream(nullptr), | |
m_audio_stream_id(-1), | |
m_audio_stream(nullptr) | |
{ | |
// WARNING: Memory leak on error - dtor not called | |
std::cout << "URL: " << m_url << std::endl; | |
if (!_M_open(m_url)) | |
throw std::runtime_error("failed to open input stream"); | |
// status = av_read_play(m_format_context); | |
} | |
ffmpeg_input::~ffmpeg_input() | |
{ | |
if (m_format_context) | |
avformat_close_input(&m_format_context); | |
if (m_frame) | |
av_frame_free(&m_frame); | |
} | |
bool | |
ffmpeg_input::read( | |
AVPacket* __packet, | |
packet_type_t* __type) | |
{ | |
packet_type_t type = packet_type_t::UNKNOWN; | |
auto success = av_read_frame(m_format_context, __packet) >= 0; | |
if (__packet->stream_index == m_video_stream_id) | |
type = packet_type_t::VIDEO; | |
else if (__packet->stream_index == m_audio_stream_id) | |
type = packet_type_t::AUDIO; | |
if (__type) *__type = type; | |
return success; | |
} | |
std::shared_ptr<stream_desc_t> | |
ffmpeg_input::video_desc() const | |
{ return _M_stream2desc(m_video_stream_id, false); } | |
std::shared_ptr<stream_desc_t> | |
ffmpeg_input::audio_desc() const | |
{ return _M_stream2desc(m_audio_stream_id, true); } | |
bool | |
ffmpeg_input::_M_open(const std::string& __source) | |
{ | |
if (!_M_open_input_stream(__source)) return false; | |
m_frame = av_frame_alloc(); | |
av_init_packet(&m_packet); | |
m_packet.data = nullptr; | |
m_packet.size = 0; | |
return _M_open_media_context(); | |
} | |
bool | |
ffmpeg_input::_M_open_input_stream(const std::string& __source) | |
{ | |
AVDictionary* format_options = NULL; | |
av_dict_set(&format_options, "rtsp_transport", "tcp", 0); | |
av_dict_set(&format_options, "stimeout", "10000000", 0); | |
bool success = true; | |
auto result = 0; | |
result = avformat_open_input(&m_format_context, | |
__source.c_str(), NULL, &format_options); | |
if (result < 0) { | |
char buffer[100]; | |
av_strerror(result, buffer, 100); | |
std::cerr << "ERROR: Could not open source " << __source | |
<< " [ " << result << ": " << buffer << " ]" << std::endl; | |
success = false; | |
} | |
else { | |
result = avformat_find_stream_info(m_format_context, NULL); | |
if (result < 0) { | |
std::cerr << "ERROR: Could not find stream information" << std::endl; | |
success = false; | |
} | |
} | |
av_dict_free(&format_options); | |
return success; | |
} | |
inline bool | |
ffmpeg_input::_M_open_media_context() | |
{ | |
bool video_found = false; | |
auto status = 0; | |
status = _M_open_codec_context( | |
&m_video_stream_id, | |
m_format_context, | |
&m_video_stream, | |
AVMEDIA_TYPE_VIDEO); | |
if (status >= 0) video_found = true; | |
status = _M_open_codec_context( | |
&m_audio_stream_id, | |
m_format_context, | |
&m_audio_stream, | |
AVMEDIA_TYPE_AUDIO); | |
if (status < 0) { | |
std::cerr << "WARNING: failed to open audio stream" << std::endl; | |
m_audio_stream_id = -1; | |
} | |
return video_found; | |
} | |
int | |
ffmpeg_input::_M_open_codec_context( | |
int* __stream_id, | |
AVFormatContext* __format_context, | |
AVStream** __stream, | |
AVMediaType __type) | |
{ | |
int status; | |
status = av_find_best_stream(__format_context, __type, -1, -1, NULL, 0); | |
if (status < 0) { | |
std::cerr << "ERROR: Could not find " << | |
av_get_media_type_string(__type) << " stream" << std::endl; | |
return status; | |
} | |
*__stream_id = status; | |
*__stream = __format_context->streams[*__stream_id]; | |
return 0; | |
} | |
std::shared_ptr<stream_desc_t> | |
ffmpeg_input::_M_stream2desc( | |
int __index, | |
bool __isaudio) const | |
{ | |
if (__index < 0) nullptr; | |
auto desc = std::make_shared<stream_desc_t>(); | |
memset(desc.get(), 0, sizeof(stream_desc_t)); | |
const auto& stream = m_format_context->streams[__index]; | |
desc->bit_rate = stream->codecpar->bit_rate; | |
if (__isaudio) { | |
desc->channels = stream->codecpar->channels; | |
desc->media_type = media_type_t::VIDEO; | |
} | |
else { | |
desc->width = stream->codecpar->width; | |
desc->height = stream->codecpar->height; | |
desc->media_type = media_type_t::AUDIO; | |
} | |
desc->sample_rate = stream->codecpar->sample_rate; | |
desc->timebase.numerator = stream->time_base.num; | |
desc->timebase.denominator = stream->time_base.den; | |
desc->codec_name = avcodec_get_name(stream->codecpar->codec_id); | |
return desc; | |
} | |
// ================================== | |
// ---------- ffmpeg_output ---------- | |
// ================================== | |
ffmpeg_output::ffmpeg_output( | |
std::string __video_url, | |
std::shared_ptr<stream_desc_t> __video_desc, | |
std::string __audio_url, | |
std::shared_ptr<stream_desc_t> __audio_desc) | |
: m_video_url(__video_url), | |
m_audio_url(__audio_url), | |
m_format(nullptr), | |
m_video_context(nullptr), | |
m_audio_context(nullptr), | |
m_video_stream(nullptr), | |
m_audio_stream(nullptr) | |
{ | |
// WARNING: Memory leak on error - dtor not called | |
int status; | |
m_format = av_guess_format("rtp", NULL, NULL); | |
if (m_format == NULL) | |
throw std::runtime_error("format not available"); | |
if (__video_desc) { | |
auto success = _M_open_video( | |
__video_url, | |
__video_desc, | |
&m_video_context, | |
&m_video_stream); | |
if (!success) | |
throw std::runtime_error("Failed to open video stream"); | |
status = avformat_write_header(m_video_context, /*options=*/NULL); | |
// if (status == AVERROR) | |
} | |
if (__audio_desc) { | |
auto success = _M_open_audio( | |
__audio_url, | |
__audio_desc, | |
&m_audio_context, | |
&m_audio_stream); | |
if (!success) | |
throw std::runtime_error("Failed to open audio stream"); | |
status = avformat_write_header(m_audio_context, /*options=*/NULL); | |
// if (status == AVERROR) | |
} | |
} | |
ffmpeg_output::~ffmpeg_output() | |
{ | |
if (m_audio_context) { | |
avio_close(m_audio_context->pb); | |
avformat_free_context(m_audio_context); | |
} | |
if (m_video_context) { | |
avio_close(m_video_context->pb); | |
avformat_free_context(m_video_context); | |
} | |
} | |
bool | |
ffmpeg_output::write_video(AVPacket* __packet) | |
{ | |
if (!m_video_context) return false; | |
__packet->stream_index = m_video_stream->index; | |
auto status = av_interleaved_write_frame(m_video_context, __packet); | |
av_packet_unref(__packet); | |
return status == 0; | |
} | |
bool | |
ffmpeg_output::write_audio(AVPacket* __packet) | |
{ | |
if (!m_audio_context) return false; | |
__packet->stream_index = m_audio_stream->index; | |
auto status = av_interleaved_write_frame(m_audio_context, __packet); | |
av_packet_unref(__packet); | |
return status == 0; | |
} | |
bool | |
ffmpeg_output::_M_open_video( | |
std::string __url, | |
std::shared_ptr<stream_desc_t> __desc, | |
AVFormatContext** __context, | |
AVStream** __stream) | |
{ | |
int status; | |
// create context | |
status = avformat_alloc_output_context2( | |
__context, | |
m_format, | |
m_format->name, | |
__url.c_str()); | |
if (status < 0) { | |
std::cerr << "error: avformat_alloc_output_context2; line: " << __LINE__ << std::endl; | |
return false; | |
} | |
// open IO interface | |
status = avio_open( | |
&(*__context)->pb, | |
__url.c_str(), | |
AVIO_FLAG_WRITE); | |
if (status < 0) { | |
std::cerr << "error: avio_open; line: " << __LINE__ << std::endl; | |
return false; | |
} | |
// set stream description | |
AVCodec* codec = avcodec_find_encoder_by_name(__desc->codec_name.c_str()); | |
if (!codec) { | |
auto name = __desc->codec_name; | |
std::transform(name.begin(), name.end(), name.begin(), | |
[](unsigned char c){ return std::tolower(c); }); | |
if (name == "h264") name = "libopenh264"; | |
codec = avcodec_find_encoder_by_name(name.c_str()); | |
if (!codec) { | |
std::cerr << "error: avcodec_find_encoder_by_name; line: " << __LINE__ << std::endl; | |
std::cerr << "Input: " << __desc->codec_name << std::endl; | |
return false; | |
} | |
} | |
*__stream = avformat_new_stream(*__context, codec); | |
(*__stream)->codecpar->codec_id = codec->id; | |
(*__stream)->codecpar->bit_rate = __desc->bit_rate; | |
(*__stream)->codecpar->width = __desc->width; | |
(*__stream)->codecpar->height = __desc->height; | |
(*__stream)->codecpar->sample_rate = __desc->sample_rate; | |
(*__stream)->time_base.num = __desc->timebase.numerator; | |
(*__stream)->time_base.den = __desc->timebase.denominator; | |
(*__stream)->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; | |
return true; | |
} | |
bool | |
ffmpeg_output::_M_open_audio( | |
std::string __url, | |
std::shared_ptr<stream_desc_t> __desc, | |
AVFormatContext** __context, | |
AVStream** __stream) | |
{ | |
int status; | |
// create context | |
status = avformat_alloc_output_context2( | |
__context, | |
m_format, | |
m_format->name, | |
__url.c_str()); | |
if (status < 0) { | |
std::cerr << "error: avformat_alloc_output_context2; line: " << __LINE__ << std::endl; | |
return false; | |
} | |
// open IO interface | |
status = avio_open( | |
&(*__context)->pb, | |
__url.c_str(), | |
AVIO_FLAG_WRITE); | |
if (status < 0) { | |
std::cerr << "error: avio_open; line: " << __LINE__ << std::endl; | |
return false; | |
} | |
// set stream description | |
AVCodec* codec = avcodec_find_encoder_by_name(__desc->codec_name.c_str()); | |
if (!codec) { | |
std::cerr << "error: avcodec_find_encoder_by_name; line: " << __LINE__ << std::endl; | |
std::cerr << "Input: " << __desc->codec_name << std::endl; | |
return false; | |
} | |
*__stream = avformat_new_stream(*__context, codec); | |
(*__stream)->codecpar->codec_id = codec->id; | |
(*__stream)->codecpar->bit_rate = __desc->bit_rate; | |
(*__stream)->codecpar->sample_rate = __desc->sample_rate; | |
(*__stream)->codecpar->channels = __desc->channels; | |
(*__stream)->time_base.num = __desc->timebase.numerator; | |
(*__stream)->time_base.den = __desc->timebase.denominator; | |
(*__stream)->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment