#include "mxwrite.hpp" #include <cmath> #include <cstdio> #include <iostream> #include <numeric> #include <thread> extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/imgutils.h> #include <libavutil/mathematics.h> #include <libavutil/opt.h> #include <libswscale/swscale.h> } std::mutex transfer_audio_mutex; bool is_format_supported(const char* filename) { const char* ext = strrchr(filename, '.'); if (!ext) return false; return (strcmp(ext, ".mp4") == 0 || strcmp(ext, ".mkv") == 0 || strcmp(ext, ".avi") == 0 || strcmp(ext, ".mov") == 0); } void cleanup_contexts(AVFormatContext* source_ctx, AVFormatContext* dest_ctx, AVFormatContext* output_ctx) { if (source_ctx) avformat_close_input(&source_ctx); if (dest_ctx) avformat_close_input(&dest_ctx); if (output_ctx) { if (!(output_ctx->oformat->flags & AVFMT_NOFILE)) avio_closep(&output_ctx->pb); avformat_free_context(output_ctx); } } void transfer_audio(std::string_view sourceAudioFile, std::string_view destVideoFile) { std::lock_guard<std::mutex> lock(transfer_audio_mutex); if (!is_format_supported(destVideoFile.data())) { std::cerr << "Unsupported output format. Supported formats: .mp4, .mkv, .avi, .mov\n"; return; } AVFormatContext *source_ctx = nullptr, *dest_ctx = nullptr, *output_ctx = nullptr; int source_audio_idx = -1, dest_video_idx = -1, dest_audio_idx = -1; std::string temp_output = std::string(destVideoFile) + ".tmp"; if (avformat_open_input(&source_ctx, sourceAudioFile.data(), nullptr, nullptr) != 0 || avformat_open_input(&dest_ctx, destVideoFile.data(), nullptr, nullptr) != 0) { std::cerr << "Failed to open input files\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } if (avformat_find_stream_info(source_ctx, nullptr) < 0 || avformat_find_stream_info(dest_ctx, nullptr) < 0) { std::cerr << "Failed to find stream info\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } for (unsigned i = 0; i < source_ctx->nb_streams; ++i) { if (source_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { source_audio_idx = i; break; } } for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) { AVMediaType type = dest_ctx->streams[i]->codecpar->codec_type; if (type == AVMEDIA_TYPE_VIDEO) dest_video_idx = i; else if (type == AVMEDIA_TYPE_AUDIO) dest_audio_idx = i; } if (source_audio_idx == -1 || dest_video_idx == -1) { std::cerr << "Required streams not found\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } const AVOutputFormat *output_fmt = av_guess_format(nullptr, destVideoFile.data(), nullptr); if (!output_fmt) { output_fmt = av_guess_format("mp4", nullptr, nullptr); if (!output_fmt) { std::cerr << "Failed to determine output format\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } } if (avformat_alloc_output_context2(&output_ctx, output_fmt, nullptr, temp_output.c_str()) < 0) { std::cerr << "Failed to create output context\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } const AVCodec* audio_codec = avcodec_find_decoder(source_ctx->streams[source_audio_idx]->codecpar->codec_id); if (!audio_codec) { std::cerr << "Failed to find audio decoder\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) { if (dest_ctx->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { continue; } AVStream *dest_stream = dest_ctx->streams[i]; AVStream *out_stream = avformat_new_stream(output_ctx, nullptr); if (!out_stream) { std::cerr << "Failed to create output stream\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } if (avcodec_parameters_copy(out_stream->codecpar, dest_stream->codecpar) < 0) { std::cerr << "Failed to copy video parameters\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } out_stream->time_base = dest_stream->time_base; out_stream->codecpar->codec_tag = 0; } AVStream *out_stream = avformat_new_stream(output_ctx, audio_codec); if (!out_stream) { std::cerr << "Failed to create audio stream\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } AVCodecParameters* source_params = source_ctx->streams[source_audio_idx]->codecpar; if (avcodec_parameters_copy(out_stream->codecpar, source_params) < 0) { std::cerr << "Failed to copy audio parameters\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } if (source_params->frame_size == 0) { out_stream->codecpar->frame_size = 1024; } else { out_stream->codecpar->frame_size = source_params->frame_size; } out_stream->time_base = source_ctx->streams[source_audio_idx]->time_base; out_stream->codecpar->codec_tag = 0; dest_audio_idx = out_stream->index; if (!(output_ctx->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&output_ctx->pb, temp_output.c_str(), AVIO_FLAG_WRITE) < 0) { std::cerr << "Failed to open output file\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } } if (avformat_write_header(output_ctx, nullptr) < 0) { std::cerr << "Failed to write header\n"; cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } AVPacket packet; while (av_read_frame(dest_ctx, &packet) >= 0) { if (packet.stream_index == dest_audio_idx) { av_packet_unref(&packet); continue; } AVStream *in_stream = dest_ctx->streams[packet.stream_index]; AVStream *out_stream = output_ctx->streams[packet.stream_index]; av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base); if (av_interleaved_write_frame(output_ctx, &packet) < 0) { std::cerr << "Failed to write packet\n"; av_packet_unref(&packet); cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } av_packet_unref(&packet); } av_seek_frame(source_ctx, source_audio_idx, 0, AVSEEK_FLAG_BACKWARD); while (av_read_frame(source_ctx, &packet) >= 0) { if (packet.stream_index == source_audio_idx) { AVStream *in_stream = source_ctx->streams[packet.stream_index]; AVStream *out_stream = output_ctx->streams[dest_audio_idx]; av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base); packet.stream_index = dest_audio_idx; if (av_interleaved_write_frame(output_ctx, &packet) < 0) { std::cerr << "Failed to write audio packet\n"; av_packet_unref(&packet); cleanup_contexts(source_ctx, dest_ctx, output_ctx); return; } } av_packet_unref(&packet); } av_write_trailer(output_ctx); cleanup_contexts(source_ctx, dest_ctx, output_ctx); std::remove(destVideoFile.data()); std::rename(temp_output.c_str(), destVideoFile.data()); } void Writer::calculateFPSFraction(float fps, int &fps_num, int &fps_den) { const float epsilon = 0.001f; fps_den = 1001; if (std::fabs(fps - 29.97f) < epsilon) { fps_num = 30000; fps_den = 1001; } else if (std::fabs(fps - 59.94f) < epsilon) { fps_num = 60000; fps_den = 1001; } else { float precision = 1000.0f; fps_num = static_cast<int>(std::round(fps * precision)); fps_den = static_cast<int>(precision); int gcd = std::gcd(fps_num, fps_den); fps_num /= gcd; fps_den /= gcd; } } bool Writer::open(const std::string& filename, int w, int h, float fps, const char *crf) { std::lock_guard<std::mutex> lock(writer_mutex); avformat_network_init(); av_log_set_level(AV_LOG_ERROR); opened = false; if (avformat_alloc_output_context2(&format_ctx, nullptr, "mp4", filename.c_str()) < 0) { std::cerr << "Could not allocate output context.\n"; return false; } const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264); if (!codec) { std::cerr << "Could not find H.264 encoder.\n"; avformat_free_context(format_ctx); return false; } stream = avformat_new_stream(format_ctx, codec); if (!stream) { std::cerr << "Could not create new stream.\n"; avformat_free_context(format_ctx); return false; } width = w; height = h; calculateFPSFraction(fps, fps_num, fps_den); AVRational tb = {fps_den, fps_num}; stream->time_base = tb; codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { std::cerr << "Could not allocate codec context.\n"; avformat_free_context(format_ctx); return false; } codec_ctx->width = width; codec_ctx->height = height; codec_ctx->time_base = stream->time_base; codec_ctx->framerate = AVRational{fps_num, fps_den}; codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; codec_ctx->gop_size = 30; codec_ctx->max_b_frames = 0; codec_ctx->thread_count = std::thread::hardware_concurrency(); codec_ctx->thread_type = FF_THREAD_SLICE; codec_ctx->slices = 4; av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", 0); av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0); av_opt_set(codec_ctx->priv_data, "crf", crf, 0); av_opt_set(codec_ctx->priv_data, "x264-params", "bframes=0:ref=1:me=dia:subme=0", 0); av_opt_set(codec_ctx->priv_data, "force_cfr", "1", 0); AVBufferRef *hw_device_ctx = nullptr; if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0) == 0) { codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); std::cout << "MXWrite: hardware acceleration enabled (CUDA)\n"; } else if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) == 0) { codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); std::cout << "MXWrite: hardware acceleration enabled (VAAPI)\n"; } else { std::cerr << "MXWrite: hardware acceleration not available, using CPU encoding\n"; } time_base = tb; if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) { codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } if (avcodec_open2(codec_ctx, codec, nullptr) < 0) { std::cerr << "Could not open codec.\n"; avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) { std::cerr << "Could not copy codec parameters.\n"; avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } if (!(format_ctx->flags & AVFMT_NOFILE)) { if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) { std::cerr << "Could not open output file: " << filename << "\n"; avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } } if (avformat_write_header(format_ctx, nullptr) < 0) { std::cerr << "Error writing MP4 header.\n"; avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } frameRGBA = av_frame_alloc(); if (!frameRGBA) { std::cerr << "Could not allocate frame.\n"; avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } frameRGBA->format = AV_PIX_FMT_RGBA; frameRGBA->width = width; frameRGBA->height = height; if (av_frame_get_buffer(frameRGBA, 32) < 0) { std::cerr << "Could not allocate frame buffer for RGBA frame.\n"; av_frame_free(&frameRGBA); avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } frameYUV = av_frame_alloc(); if (!frameYUV) { std::cerr << "Could not allocate YUV frame.\n"; av_frame_free(&frameRGBA); avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } frameYUV->format = AV_PIX_FMT_YUV420P; frameYUV->width = width; frameYUV->height = height; if (av_frame_get_buffer(frameYUV, 32) < 0) { std::cerr << "Could not allocate frame buffer for YUV frame.\n"; av_frame_free(&frameRGBA); av_frame_free(&frameYUV); avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr); if (!sws_ctx) { std::cerr << "Could not initialize the conversion context.\n"; av_frame_free(&frameRGBA); av_frame_free(&frameYUV); avio_closep(&format_ctx->pb); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); return false; } opened = true; recordingStart = std::chrono::steady_clock::now(); return true; } void Writer::write(void* rgba_buffer) { std::lock_guard<std::mutex> lock(writer_mutex); if (!opened || !rgba_buffer) { return; } { std::lock_guard<std::mutex> frame_lock(frame_mutex); if (frame_queue.size() >= MAX_QUEUE_SIZE) { static int drop_counter = 0; if (++drop_counter % 30 == 0) { std::cerr << "Writer: dropped " << drop_counter << " frames (encoder too slow)\n"; } return; } } std::lock_guard<std::mutex> frame_lock(frame_mutex); memcpy(frameRGBA->data[0], rgba_buffer, width * height * 4); sws_scale(sws_ctx, frameRGBA->data, frameRGBA->linesize, 0, height, frameYUV->data, frameYUV->linesize); frameYUV->pts = frame_count++; int ret = avcodec_send_frame(codec_ctx, frameYUV); if (ret < 0) { std::cerr << "Error sending frame to encoder: " << ret << std::endl; return; } AVPacket* pkt = av_packet_alloc(); while (true) { ret = avcodec_receive_packet(codec_ctx, pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { std::cerr << "Error receiving packet: " << ret << std::endl; av_packet_free(&pkt); return; } av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base); pkt->stream_index = stream->index; if (av_interleaved_write_frame(format_ctx, pkt) < 0) { std::cerr << "Error writing frame.\n"; av_packet_free(&pkt); return; } av_packet_unref(pkt); } av_packet_free(&pkt); } bool Writer::open_ts(const std::string& filename, int w, int h, float fps, const char *crf) { avformat_network_init(); av_log_set_level(AV_LOG_ERROR); opened = false; if (avformat_alloc_output_context2(&format_ctx, nullptr, "mp4", filename.c_str()) < 0) { std::cerr << "Could not allocate output context.\n"; return false; } const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264); if (!codec) { std::cerr << "Could not find H.264 encoder.\n"; avformat_free_context(format_ctx); return false; } stream = avformat_new_stream(format_ctx, codec); if (!stream) { std::cerr << "Could not create new stream.\n"; avformat_free_context(format_ctx); return false; } width = w; height = h; calculateFPSFraction(fps, fps_num, fps_den); AVRational precise_tb = { fps_den, fps_num }; stream->time_base = precise_tb; codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { std::cerr << "Could not allocate codec context.\n"; avformat_free_context(format_ctx); return false; } codec_ctx->width = width; codec_ctx->height = height; codec_ctx->time_base = precise_tb; codec_ctx->framerate = AVRational{ fps_num, fps_den }; codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; codec_ctx->gop_size = 30; codec_ctx->thread_count = std::thread::hardware_concurrency(); codec_ctx->thread_type = FF_THREAD_SLICE; codec_ctx->slices = 4; codec_ctx->max_b_frames = 0; codec_ctx->delay = 0; codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", 0); av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0); av_opt_set(codec_ctx->priv_data, "crf", crf, 0); av_opt_set(codec_ctx->priv_data, "x264-params", "bframes=0:ref=1:me=dia:subme=0", 0); av_opt_set(codec_ctx->priv_data, "force_cfr", "1", 0); AVBufferRef *hw_device_ctx = nullptr; if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0) == 0) { codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); std::cout << "MXWrite: hardware acceleration enabled (CUDA) for TS\n"; } else if (av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0) == 0) { codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); std::cout << "MXWrite: hardware acceleration enabled (VAAPI) for TS\n"; } else { std::cerr << "MXWrite: hardware acceleration not available for TS, using CPU encoding\n"; } if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) { codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } if (avcodec_open2(codec_ctx, codec, nullptr) < 0) { std::cerr << "Could not open codec.\n"; return false; } if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) { std::cerr << "Could not open output file.\n"; return false; } } if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) { std::cerr << "Failed to copy codec parameters.\n"; return false; } if (avformat_write_header(format_ctx, nullptr) < 0) { std::cerr << "Error occurred when writing header.\n"; return false; } frameRGBA = av_frame_alloc(); if (!frameRGBA) { std::cerr << "Could not allocate frameRGBA.\n"; return false; } frameRGBA->format = AV_PIX_FMT_RGBA; frameRGBA->width = width; frameRGBA->height = height; if (av_frame_get_buffer(frameRGBA, 32) < 0) { std::cerr << "Could not allocate frame buffer for RGBA frame.\n"; return false; } frameYUV = av_frame_alloc(); if (!frameYUV) { std::cerr << "Could not allocate frameYUV.\n"; return false; } frameYUV->format = AV_PIX_FMT_YUV420P; frameYUV->width = width; frameYUV->height = height; if (av_frame_get_buffer(frameYUV, 32) < 0) { std::cerr << "Could not allocate frame buffer for YUV frame.\n"; return false; } sws_ctx = sws_getContext( width, height, AV_PIX_FMT_RGBA, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr ); if (!sws_ctx) { std::cerr << "Could not create sws context.\n"; return false; } opened = true; frame_count = 0; return true; } void Writer::write_ts(void* rgba_buffer) { std::lock_guard<std::mutex> lock(writer_mutex); if (!opened || !rgba_buffer) { return; } const int src_stride = width * 4; uint8_t* src_data[1] = { static_cast<uint8_t*>(rgba_buffer) }; int src_linesize[1] = { src_stride }; sws_scale(sws_ctx, src_data, src_linesize, 0, height, frameYUV->data, frameYUV->linesize); frameYUV->pts = frame_count; int ret = avcodec_send_frame(codec_ctx, frameYUV); if (ret < 0) { std::cerr << "Error sending frame to encoder: " << ret << "\n"; return; } frame_count++; AVPacket* pkt = av_packet_alloc(); while (true) { ret = avcodec_receive_packet(codec_ctx, pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { std::cerr << "Error receiving packet: " << ret << "\n"; av_packet_free(&pkt); return; } av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base); pkt->stream_index = stream->index; if (av_interleaved_write_frame(format_ctx, pkt) < 0) { std::cerr << "Error writing frame.\n"; av_packet_free(&pkt); return; } av_packet_unref(pkt); } av_packet_free(&pkt); } void Writer::close() { std::lock_guard<std::mutex> lock(writer_mutex); if (!opened) { return; } if (stream && stream->duration > 0) { last_duration = static_cast<double>(stream->duration) * av_q2d(stream->time_base); } else if (fps_num > 0 && fps_den > 0) { last_duration = static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num); } AVPacket* pkt = av_packet_alloc(); avcodec_send_frame(codec_ctx, nullptr); while (true) { int ret = avcodec_receive_packet(codec_ctx, pkt); if (ret == AVERROR_EOF || ret < 0) { break; } av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base); pkt->stream_index = stream->index; av_interleaved_write_frame(format_ctx, pkt); av_packet_unref(pkt); } av_packet_free(&pkt); av_write_trailer(format_ctx); if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) { avio_closep(&format_ctx->pb); } av_frame_free(&frameRGBA); av_frame_free(&frameYUV); sws_freeContext(sws_ctx); avcodec_free_context(&codec_ctx); avformat_free_context(format_ctx); opened = false; format_ctx = nullptr; codec_ctx = nullptr; sws_ctx = nullptr; frameRGBA = nullptr; frameYUV = nullptr; } double Writer::get_duration() const { if (!opened && last_duration > 0.0) { return last_duration; } if (stream && stream->duration > 0) { return static_cast<double>(stream->duration) * av_q2d(stream->time_base); } if (fps_num > 0 && fps_den > 0) { return static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num); } return 0.0; }