You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
4.3 KiB
167 lines
4.3 KiB
4 months ago
|
#include "tools/clip/recorder/ffmpeg.h"
|
||
4 months ago
|
#include <QDebug>
|
||
|
|
||
|
FFmpegEncoder::FFmpegEncoder(const QString& outputFile, int width, int height, int fps) {
|
||
|
// Allocate output context
|
||
4 months ago
|
if (avformat_alloc_output_context2(&format_ctx, nullptr, nullptr, outputFile.toStdString().c_str()) < 0) {
|
||
4 months ago
|
return;
|
||
|
}
|
||
|
|
||
4 months ago
|
// Find H.264 encoder
|
||
4 months ago
|
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
|
||
|
if (!codec) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Create video stream
|
||
|
stream = avformat_new_stream(format_ctx, codec);
|
||
|
if (!stream) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Create codec context
|
||
|
codec_ctx = avcodec_alloc_context3(codec);
|
||
|
if (!codec_ctx) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Set codec parameters
|
||
|
codec_ctx->codec_id = AV_CODEC_ID_H264;
|
||
|
codec_ctx->width = width;
|
||
|
codec_ctx->height = height;
|
||
4 months ago
|
codec_ctx->time_base = {1, fps};
|
||
|
codec_ctx->framerate = {fps, 1};
|
||
4 months ago
|
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||
4 months ago
|
codec_ctx->gop_size = 12;
|
||
4 months ago
|
codec_ctx->max_b_frames = 0;
|
||
|
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||
|
|
||
4 months ago
|
stream->time_base = codec_ctx->time_base;
|
||
|
|
||
|
// Set encoding options
|
||
4 months ago
|
AVDictionary* opts = nullptr;
|
||
|
av_dict_set(&opts, "preset", "ultrafast", 0);
|
||
4 months ago
|
av_dict_set(&opts, "tune", "zerolatency", 0);
|
||
4 months ago
|
av_dict_set(&opts, "crf", "28", 0);
|
||
|
|
||
4 months ago
|
// Open codec
|
||
4 months ago
|
if (avcodec_open2(codec_ctx, codec, &opts) < 0) {
|
||
|
av_dict_free(&opts);
|
||
|
return;
|
||
|
}
|
||
|
av_dict_free(&opts);
|
||
|
|
||
|
// Copy codec parameters to stream
|
||
|
if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
stream->time_base = codec_ctx->time_base;
|
||
|
|
||
|
// Open output file
|
||
|
if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
|
||
|
if (avio_open(&format_ctx->pb, outputFile.toStdString().c_str(), AVIO_FLAG_WRITE) < 0) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
4 months ago
|
// Write header
|
||
|
if (avformat_write_header(format_ctx, nullptr) < 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
4 months ago
|
// Allocate frame
|
||
|
frame = av_frame_alloc();
|
||
|
if (!frame) {
|
||
|
return;
|
||
|
}
|
||
4 months ago
|
frame->format = AV_PIX_FMT_YUV420P;
|
||
4 months ago
|
frame->width = width;
|
||
|
frame->height = height;
|
||
4 months ago
|
if (av_frame_get_buffer(frame, 0) < 0) {
|
||
4 months ago
|
return;
|
||
|
}
|
||
|
|
||
|
// Create scaling context
|
||
|
sws_ctx = sws_getContext(width, height, AV_PIX_FMT_BGRA,
|
||
4 months ago
|
width, height, AV_PIX_FMT_YUV420P,
|
||
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||
|
if (!sws_ctx) {
|
||
|
return;
|
||
|
}
|
||
4 months ago
|
|
||
|
// Allocate packet
|
||
|
packet = av_packet_alloc();
|
||
|
if (!packet) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
initialized = true;
|
||
|
}
|
||
|
|
||
|
FFmpegEncoder::~FFmpegEncoder() {
|
||
|
if (initialized) {
|
||
4 months ago
|
// encodeFrame(nullptr); // Flush encoder
|
||
4 months ago
|
av_write_trailer(format_ctx);
|
||
4 months ago
|
if (!(format_ctx->oformat->flags & AVFMT_NOFILE) && format_ctx->pb) {
|
||
4 months ago
|
avio_closep(&format_ctx->pb);
|
||
|
}
|
||
|
}
|
||
|
|
||
4 months ago
|
av_frame_free(&frame);
|
||
|
av_packet_free(&packet);
|
||
|
sws_freeContext(sws_ctx);
|
||
|
avcodec_free_context(&codec_ctx);
|
||
|
avformat_free_context(format_ctx);
|
||
4 months ago
|
}
|
||
|
|
||
|
bool FFmpegEncoder::writeFrame(const QImage& image) {
|
||
|
if (!initialized) return false;
|
||
|
|
||
|
// Convert BGRA to YUV420P
|
||
4 months ago
|
uint8_t* inData[1] = { (uint8_t*)image.bits() };
|
||
|
int inLinesize[1] = { image.bytesPerLine() };
|
||
4 months ago
|
sws_scale(sws_ctx, inData, inLinesize, 0, image.height(),
|
||
|
frame->data, frame->linesize);
|
||
|
|
||
4 months ago
|
frame->pts = frame_count; // PTS in codec_ctx->time_base units
|
||
|
return encodeFrame(frame);
|
||
|
}
|
||
4 months ago
|
|
||
4 months ago
|
bool FFmpegEncoder::encodeFrame(AVFrame* input_frame) {
|
||
|
int ret = avcodec_send_frame(codec_ctx, input_frame);
|
||
4 months ago
|
if (ret < 0) {
|
||
4 months ago
|
fprintf(stderr, "Failed to send frame: %d\n", ret);
|
||
4 months ago
|
return false;
|
||
|
}
|
||
|
|
||
4 months ago
|
while (true) {
|
||
4 months ago
|
ret = avcodec_receive_packet(codec_ctx, packet);
|
||
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||
|
break;
|
||
4 months ago
|
}
|
||
|
if (ret < 0) {
|
||
4 months ago
|
fprintf(stderr, "Error receiving packet: %d\n", ret);
|
||
4 months ago
|
return false;
|
||
|
}
|
||
|
|
||
4 months ago
|
// Set packet timestamps and duration, rescaling if necessary
|
||
4 months ago
|
packet->pts = av_rescale_q(frame_count, codec_ctx->time_base, stream->time_base);
|
||
4 months ago
|
packet->dts = packet->pts; // No B-frames, so DTS = PTS
|
||
4 months ago
|
packet->duration = av_rescale_q(1, codec_ctx->time_base, stream->time_base);
|
||
|
packet->stream_index = stream->index;
|
||
4 months ago
|
|
||
4 months ago
|
ret = av_interleaved_write_frame(format_ctx, packet);
|
||
4 months ago
|
av_packet_unref(packet);
|
||
|
|
||
4 months ago
|
if (ret < 0) {
|
||
4 months ago
|
fprintf(stderr, "Failed to write packet: %d\n", ret);
|
||
4 months ago
|
return false;
|
||
|
}
|
||
|
|
||
4 months ago
|
if (input_frame) {
|
||
|
frame_count++; // Only increment on actual frames, not flushing
|
||
|
}
|
||
4 months ago
|
}
|
||
|
return true;
|
||
|
}
|