/*
* Copyright (C) 2023 Mikhail Burakov. This file is part of streamer.
*
* streamer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* streamer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with streamer. If not, see .
*/
#include "encode.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "gpu.h"
#include "perf.h"
#include "util.h"
struct EncodeContext {
struct GpuContext* gpu_context;
AVBufferRef* hwdevice_context;
AVCodecContext* codec_context;
AVFrame* hw_frame;
struct GpuFrame* gpu_frame;
};
static bool SetHwFramesContext(struct EncodeContext* encode_context, int width,
int height) {
encode_context->codec_context->hw_frames_ctx =
av_hwframe_ctx_alloc(encode_context->hwdevice_context);
if (!encode_context->codec_context->hw_frames_ctx) {
LOG("Failed to allocate hwframes context");
return false;
}
AVHWFramesContext* hwframes_context_data =
(void*)(encode_context->codec_context->hw_frames_ctx->data);
hwframes_context_data->initial_pool_size = 8;
hwframes_context_data->format = AV_PIX_FMT_VAAPI;
hwframes_context_data->sw_format = AV_PIX_FMT_NV12;
hwframes_context_data->width = width;
hwframes_context_data->height = height;
int err = av_hwframe_ctx_init(encode_context->codec_context->hw_frames_ctx);
if (err < 0) {
LOG("Failed to init hwframes context (%s)", av_err2str(err));
av_buffer_unref(&encode_context->codec_context->hw_frames_ctx);
return false;
}
return true;
}
static enum AVColorSpace ConvertColorspace(enum YuvColorspace colorspace) {
switch (colorspace) {
case kItuRec601:
// TODO(mburakov): No dedicated definition for BT601?
return AVCOL_SPC_SMPTE170M;
case kItuRec709:
return AVCOL_SPC_BT709;
default:
__builtin_unreachable();
}
}
static enum AVColorRange ConvertRange(enum YuvRange range) {
switch (range) {
case kNarrowRange:
return AVCOL_RANGE_MPEG;
case kFullRange:
return AVCOL_RANGE_JPEG;
default:
__builtin_unreachable();
}
}
struct EncodeContext* EncodeContextCreate(struct GpuContext* gpu_context,
uint32_t width, uint32_t height,
enum YuvColorspace colrospace,
enum YuvRange range) {
struct EncodeContext* encode_context = malloc(sizeof(struct EncodeContext));
if (!encode_context) {
LOG("Failed to allocate encode context (%s)", strerror(errno));
return NULL;
}
*encode_context = (struct EncodeContext){
.gpu_context = gpu_context,
};
int err = av_hwdevice_ctx_create(&encode_context->hwdevice_context,
AV_HWDEVICE_TYPE_VAAPI, NULL, NULL, 0);
if (err < 0) {
LOG("Failed to create hwdevice context (%s)", av_err2str(err));
goto rollback_encode_context;
}
static const char codec_name[] = "hevc_vaapi";
const AVCodec* codec = avcodec_find_encoder_by_name(codec_name);
if (!codec) {
LOG("Failed to find %s encoder", codec_name);
goto rollback_hwdevice_context;
}
encode_context->codec_context = avcodec_alloc_context3(codec);
if (!encode_context->codec_context) {
LOG("Failed to allocate codec context");
goto rollback_hwdevice_context;
}
encode_context->codec_context->time_base = (AVRational){1, 60};
encode_context->codec_context->width = (int)width;
encode_context->codec_context->height = (int)height;
encode_context->codec_context->pix_fmt = AV_PIX_FMT_VAAPI;
encode_context->codec_context->max_b_frames = 0;
encode_context->codec_context->refs = 1;
encode_context->codec_context->global_quality = 28;
encode_context->codec_context->colorspace = ConvertColorspace(colrospace);
encode_context->codec_context->color_range = ConvertRange(range);
if (!SetHwFramesContext(encode_context, (int)width, (int)height)) {
LOG("Failed to set hwframes context");
goto rollback_codec_context;
}
err = avcodec_open2(encode_context->codec_context, codec, NULL);
if (err < 0) {
LOG("Failed to open codec (%s)", av_err2str(err));
goto rollback_codec_context;
}
return encode_context;
rollback_codec_context:
avcodec_free_context(&encode_context->codec_context);
rollback_hwdevice_context:
av_buffer_unref(&encode_context->hwdevice_context);
rollback_encode_context:
free(encode_context);
return NULL;
}
static struct GpuFrame* PrimeToGpuFrame(
struct GpuContext* gpu_context, const VADRMPRIMESurfaceDescriptor* prime) {
struct GpuFramePlane planes[4];
for (size_t i = 0; i < prime->layers[0].num_planes; i++) {
uint32_t object_index = prime->layers[0].object_index[i];
if (prime->objects[object_index].fd == -1) break;
planes[i] = (struct GpuFramePlane){
.dmabuf_fd = prime->objects[object_index].fd,
.pitch = prime->layers[0].pitch[i],
.offset = prime->layers[0].offset[i],
.modifier = prime->objects[object_index].drm_format_modifier,
};
}
struct GpuFrame* gpu_frame =
GpuFrameCreate(gpu_context, prime->width, prime->height, prime->fourcc,
prime->layers[0].num_planes, planes);
for (size_t i = prime->num_objects; i; i--) close(prime->objects[i - 1].fd);
return gpu_frame;
}
const struct GpuFrame* EncodeContextGetFrame(
struct EncodeContext* encode_context) {
AVFrame* hw_frame = av_frame_alloc();
if (!hw_frame) {
LOG("Failed to allocate hwframe");
return NULL;
}
int err = av_hwframe_get_buffer(encode_context->codec_context->hw_frames_ctx,
hw_frame, 0);
if (err < 0) {
LOG("Failed to get hwframe buffer (%s)", av_err2str(err));
goto rollback_hw_frame;
}
if (!hw_frame->hw_frames_ctx) {
LOG("Failed to ref hwframe context");
goto rollback_hw_frame;
}
// mburakov: Roughly based on Sunshine code...
AVVAAPIDeviceContext* vaapi_device_context =
((AVHWDeviceContext*)(void*)encode_context->hwdevice_context->data)
->hwctx;
VASurfaceID surface_id = (VASurfaceID)(uintptr_t)hw_frame->data[3];
VADRMPRIMESurfaceDescriptor prime;
VAStatus status = vaExportSurfaceHandle(
vaapi_device_context->display, surface_id,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_COMPOSED_LAYERS, &prime);
if (status != VA_STATUS_SUCCESS) {
LOG("Failed to export vaapi surface (%d)", status);
goto rollback_hw_frame;
}
struct GpuFrame* gpu_frame =
PrimeToGpuFrame(encode_context->gpu_context, &prime);
if (!gpu_frame) {
LOG("Failed to create gpu frame");
goto rollback_hw_frame;
}
encode_context->hw_frame = hw_frame;
encode_context->gpu_frame = gpu_frame;
return gpu_frame;
rollback_hw_frame:
av_frame_free(&hw_frame);
return NULL;
}
static bool DrainPacket(const struct AVPacket* packet, int fd) {
uint32_t size = (uint32_t)packet->size;
struct iovec iov[] = {
{.iov_base = &size, .iov_len = sizeof(size)},
{.iov_base = packet->data, .iov_len = (size_t)packet->size},
};
for (;;) {
ssize_t result = writev(fd, iov, LENGTH(iov));
switch (result) {
case -1:
if (errno == EINTR) continue;
if (errno == EPIPE) return true;
LOG("Failed to write packed (%s)", strerror(errno));
return false;
case 0:
LOG("Output file descriptor closed");
return false;
default:
break;
}
for (size_t i = 0; i < LENGTH(iov); i++) {
size_t delta = MIN((size_t)result, iov[i].iov_len);
iov[i].iov_base = (uint8_t*)iov[i].iov_base + delta;
iov[i].iov_len -= delta;
result -= delta;
}
if (!result) return true;
}
}
bool EncodeContextEncodeFrame(struct EncodeContext* encode_context, int fd,
struct TimingStats* encode,
struct TimingStats* drain) {
bool result = false;
unsigned long long before_send = MicrosNow();
if (encode_context->gpu_frame) {
GpuFrameDestroy(encode_context->gpu_frame);
encode_context->gpu_frame = NULL;
}
AVPacket* packet = av_packet_alloc();
if (!packet) {
LOG("Failed to allocate packet (%s)", strerror(errno));
goto rollback_hw_frame;
}
int err = avcodec_send_frame(encode_context->codec_context,
encode_context->hw_frame);
if (err < 0) {
LOG("Failed to send frame (%s)", av_err2str(err));
goto rollback_packet;
}
unsigned long long total_send = MicrosNow() - before_send;
unsigned long long total_receive = 0;
unsigned long long total_drain = 0;
for (;;) {
unsigned long long before_receive = MicrosNow();
err = avcodec_receive_packet(encode_context->codec_context, packet);
switch (err) {
case 0:
break;
case AVERROR(EAGAIN):
case AVERROR_EOF:
total_receive += MicrosNow() - before_receive;
if (encode) TimingStatsRecord(encode, total_send + total_receive);
if (drain) TimingStatsRecord(drain, total_drain);
result = true;
goto rollback_packet;
default:
LOG("Failed to receive packet (%s)", av_err2str(err));
goto rollback_packet;
}
packet->stream_index = 0;
unsigned long long before_drain = MicrosNow();
bool result = DrainPacket(packet, fd);
av_packet_unref(packet);
if (!result) {
LOG("Failed to write full packet (%s)", strerror(errno));
goto rollback_packet;
}
total_receive += before_drain - before_receive;
total_drain += MicrosNow() - before_drain;
}
rollback_packet:
av_packet_free(&packet);
rollback_hw_frame:
av_frame_free(&encode_context->hw_frame);
return result;
}
void EncodeContextDestroy(struct EncodeContext* encode_context) {
if (encode_context->gpu_frame) GpuFrameDestroy(encode_context->gpu_frame);
if (encode_context->hw_frame) av_frame_free(&encode_context->hw_frame);
avcodec_free_context(&encode_context->codec_context);
av_buffer_unref(&encode_context->hwdevice_context);
free(encode_context);
}