mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2026-07-18 00:45:51 +00:00
269 lines
9.6 KiB
C++
269 lines
9.6 KiB
C++
#include "SoftwareEncoder.h"
|
||
extern "C" {
|
||
#include <libavutil/imgutils.h>
|
||
#include <libavutil/opt.h>
|
||
}
|
||
#include "KasmVideoConstants.h"
|
||
#include <rfb/LogWriter.h>
|
||
#include <rfb/SConnection.h>
|
||
#include <rfb/ServerCore.h>
|
||
#include <rfb/encodings.h>
|
||
#include <rfb/ffmpeg.h>
|
||
#include <fmt/format.h>
|
||
|
||
static rfb::LogWriter vlog("SoftwareEncoder");
|
||
|
||
namespace rfb {
|
||
SoftwareEncoder::SoftwareEncoder(Screen layout_, const FFmpeg &ffmpeg_, SConnection *conn, KasmVideoEncoders::Encoder encoder_,
|
||
VideoEncoderParams params) :
|
||
VideoEncoder(layout_.id, conn), layout(layout_),
|
||
ffmpeg(ffmpeg_), encoder(encoder_), current_params(params), msg_codec_id(KasmVideoEncoders::to_msg_id(encoder)) {
|
||
const auto *enc_name = KasmVideoEncoders::to_string(encoder);
|
||
codec = ffmpeg.avcodec_find_encoder_by_name(enc_name);
|
||
if (!codec)
|
||
throw std::runtime_error(fmt::format("Could not find {} encoder", enc_name));
|
||
|
||
auto *frame = ffmpeg.av_frame_alloc();
|
||
if (!frame) {
|
||
throw std::runtime_error("Cannot allocate AVFrame");
|
||
}
|
||
frame_guard.reset(frame);
|
||
|
||
auto *pkt = ffmpeg.av_packet_alloc();
|
||
if (!pkt)
|
||
throw std::runtime_error("Could not allocate packet");
|
||
|
||
pkt_guard.reset(pkt);
|
||
}
|
||
|
||
bool SoftwareEncoder::isSupported() const {
|
||
return conn->cp.supportsEncoding(encodingKasmVideo);
|
||
}
|
||
|
||
bool SoftwareEncoder::render(const PixelBuffer *pb) {
|
||
// compress
|
||
int stride;
|
||
|
||
const auto rect = layout.dimensions;
|
||
const auto *buffer = pb->getBuffer(rect, &stride);
|
||
|
||
const int width = rect.width();
|
||
const int height = rect.height();
|
||
auto *frame = frame_guard.get();
|
||
|
||
int dst_width = width;
|
||
int dst_height = height;
|
||
|
||
if (width % 2 != 0)
|
||
dst_width = width & ~1;
|
||
|
||
if (height % 2 != 0)
|
||
dst_height = height & ~1;
|
||
|
||
VideoEncoderParams params{dst_width,
|
||
dst_height,
|
||
static_cast<uint8_t>(Server::frameRate),
|
||
static_cast<uint8_t>(Server::groupOfPicture),
|
||
static_cast<uint8_t>(Server::videoQualityCRFCQP)};
|
||
|
||
if (current_params != params) {
|
||
bpp = pb->getPF().bpp >> 3;
|
||
if (!init(width, height, params)) {
|
||
vlog.error("Failed to initialize encoder");
|
||
return false;
|
||
}
|
||
|
||
frame = frame_guard.get();
|
||
} else {
|
||
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||
}
|
||
|
||
const uint8_t *src_data[1] = {buffer};
|
||
const int src_line_size[1] = {stride * bpp}; // RGB has bpp bytes per pixel
|
||
|
||
if (ffmpeg.sws_scale(sws_guard.get(), src_data, src_line_size, 0, height, frame->data, frame->linesize) < 0) {
|
||
vlog.error("Error while scaling image");
|
||
return false;
|
||
}
|
||
|
||
frame->pts = pts++;
|
||
|
||
int err = ffmpeg.avcodec_send_frame(ctx_guard.get(), frame);
|
||
if (err < 0) {
|
||
vlog.error("Error sending frame to codec (%s). Error code: %d", ffmpeg.get_error_description(err).c_str(), err);
|
||
return false;
|
||
}
|
||
|
||
auto *pkt = pkt_guard.get();
|
||
|
||
err = ffmpeg.avcodec_receive_packet(ctx_guard.get(), pkt);
|
||
if (err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
|
||
// Trying one more time
|
||
err = ffmpeg.avcodec_send_frame(ctx_guard.get(), nullptr);
|
||
err = ffmpeg.avcodec_receive_packet(ctx_guard.get(), pkt);
|
||
}
|
||
|
||
if (err < 0) {
|
||
vlog.error("Error receiving packet from codec");
|
||
writeSkipRect();
|
||
return false;
|
||
}
|
||
|
||
if (pkt->flags & AV_PKT_FLAG_KEY)
|
||
vlog.debug("Key frame %ld", frame->pts);
|
||
|
||
return true;
|
||
}
|
||
|
||
void SoftwareEncoder::writeRect(const PixelBuffer *pb, const Palette &palette) {
|
||
auto *pkt = pkt_guard.get();
|
||
|
||
auto *os = conn->getOutStream(conn->cp.supportsUdp);
|
||
os->writeU8(layout.id);
|
||
os->writeU8(msg_codec_id);
|
||
os->writeU8(pkt->flags & AV_PKT_FLAG_KEY);
|
||
write_compact(os, pkt->size);
|
||
os->writeBytes(&pkt->data[0], pkt->size);
|
||
vlog.debug("Screen id %d, codec %d, frame size: %d", layout.id, msg_codec_id, pkt->size);
|
||
|
||
ffmpeg.av_packet_unref(pkt);
|
||
}
|
||
|
||
void SoftwareEncoder::writeSolidRect(int width, int height, const PixelFormat &pf, const rdr::U8 *colour) {}
|
||
|
||
void SoftwareEncoder::writeSkipRect() {
|
||
auto *os = conn->getOutStream(conn->cp.supportsUdp);
|
||
os->writeU8(layout.id);
|
||
os->writeU8(kasmVideoSkip);
|
||
}
|
||
|
||
void SoftwareEncoder::write_compact(rdr::OutStream *os, int value) {
|
||
auto b = value & 0x7F;
|
||
if (value <= 0x7F) {
|
||
os->writeU8(b);
|
||
} else {
|
||
os->writeU8(b | 0x80);
|
||
b = value >> 7 & 0x7F;
|
||
if (value <= 0x3FFF) {
|
||
os->writeU8(b);
|
||
} else {
|
||
os->writeU8(b | 0x80);
|
||
os->writeU8(value >> 14 & 0xFF);
|
||
}
|
||
}
|
||
}
|
||
|
||
bool SoftwareEncoder::init(int width, int height, VideoEncoderParams params) {
|
||
current_params = params;
|
||
vlog.debug("FRAME RESIZE (%d, %d): RATE: %d, GOP: %d, QUALITY: %d", width, height, current_params.frame_rate, current_params.group_of_picture, current_params.quality);
|
||
|
||
auto *ctx = ffmpeg.avcodec_alloc_context3(codec);
|
||
if (!ctx) {
|
||
vlog.error("Cannot allocate AVCodecContext");
|
||
return false;
|
||
}
|
||
|
||
ctx_guard.reset(ctx);
|
||
|
||
ctx->time_base = {1, params.frame_rate};
|
||
ctx->framerate = {params.frame_rate, 1};
|
||
ctx->gop_size = params.group_of_picture; // interval between I-frames
|
||
ctx->width = current_params.width;
|
||
ctx->height = current_params.height;
|
||
ctx->coded_width = current_params.width;
|
||
ctx->coded_height = current_params.height;
|
||
// best
|
||
// ctx->pix_fmt = AV_PIX_FMT_YUV444P; // AV_PIX_FMT_YUV420P;
|
||
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||
ctx->max_b_frames = 0; // No B-frames for immediate output
|
||
|
||
// HIGH
|
||
// if (ffmpeg.av_opt_set(ctx->priv_data, "tune", "zerolatency,stillimage", 0) != 0)
|
||
// return false;
|
||
//
|
||
// // start here, lower (20–22) = better quality,
|
||
// // higher (24–28) = lower bitrate
|
||
// if (ffmpeg.av_opt_set(ctx->priv_data, "crf", "18", 0) != 0)
|
||
// return false;
|
||
//
|
||
// // Preset: speed vs. compression efficiency
|
||
// if (ffmpeg.av_opt_set(ctx->priv_data, "preset", "medium", 0) != 0)
|
||
// return false;
|
||
|
||
if (ffmpeg.av_opt_set(ctx->priv_data, "tune", "zerolatency", 0) < 0) {
|
||
vlog.info("Cannot set tune to zerolatency");
|
||
}
|
||
|
||
if (ffmpeg.av_opt_set(ctx->priv_data, "preset", "ultrafast", 0) < 0) {
|
||
vlog.info("Cannot set preset to ultrafast");
|
||
}
|
||
|
||
if (encoder == KasmVideoEncoders::Encoder::av1_software) {
|
||
if (ffmpeg.av_opt_set(ctx->priv_data, "preset", "12", 0) < 0) {
|
||
vlog.info("Cannot set preset to 8");
|
||
}
|
||
|
||
if (ffmpeg.av_opt_set(ctx->priv_data, "svtav1-params", "rtc=1", 0) < 0) {
|
||
vlog.info("Cannot set -svtav1-params to tune=0");
|
||
}
|
||
}
|
||
|
||
// start here, lower (20–22) = better quality,
|
||
// higher (24–28) = lower bitrate
|
||
if (ffmpeg.av_opt_set_int(ctx->priv_data, "crf", current_params.quality, 0) < 0) {
|
||
vlog.info("Cannot set crf to %d", current_params.quality);
|
||
}
|
||
|
||
|
||
// // Preset: speed vs. compression efficiency
|
||
// if (ffmpeg.av_opt_set(ctx->priv_data, "preset", "medium", 0) != 0)
|
||
// return false;
|
||
|
||
/*if (ffmpeg.av_opt_set(ctx->priv_data, "preset", "ultrafast", 0) != 0)
|
||
throw std::runtime_error("Could not set codec setting");*/
|
||
// "ultrafast" = lowest latency but bigger bitrate
|
||
// "veryfast" = good balance for realtime
|
||
// "medium+" = too slow for live
|
||
|
||
// H.264 profile for better compression
|
||
// if (ffmpeg.av_opt_set(ctx->priv_data, "profile", "high", 0) != 0)
|
||
// throw std::runtime_error("Could not set codec setting");
|
||
|
||
auto *sws_ctx = ffmpeg.sws_getContext(width,
|
||
height,
|
||
AV_PIX_FMT_RGB32,
|
||
current_params.width,
|
||
current_params.height,
|
||
ctx_guard->pix_fmt,
|
||
SWS_BILINEAR,
|
||
nullptr,
|
||
nullptr,
|
||
nullptr);
|
||
if (!sws_ctx) {
|
||
vlog.error("Could not initialize the conversion context");
|
||
return false;
|
||
}
|
||
|
||
sws_guard.reset(sws_ctx);
|
||
|
||
auto *frame = frame_guard.get();
|
||
|
||
ffmpeg.av_frame_unref(frame);
|
||
frame->format = ctx_guard->pix_fmt;
|
||
frame->width = current_params.width;
|
||
frame->height = current_params.height;
|
||
frame->pict_type = AV_PICTURE_TYPE_I;
|
||
|
||
if (ffmpeg.av_frame_get_buffer(frame, 0) < 0) {
|
||
vlog.error("Could not allocate frame data");
|
||
return false;
|
||
}
|
||
|
||
if (ffmpeg.avcodec_open2(ctx_guard.get(), codec, nullptr) < 0) {
|
||
vlog.error("Failed to open codec");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
} // namespace rfb
|