X264 encoder example
#include <roxlu/core/Log.h> | |
#include <roxlu/core/Utils.h> | |
#include <video/X264Encoder.h> | |
X264Encoder::X264Encoder() | |
:in_width(0) | |
,in_height(0) | |
,in_pixel_format(AV_PIX_FMT_NONE) | |
,out_width(0) | |
,out_height(0) | |
,out_pixel_format(AV_PIX_FMT_NONE) | |
,fps(25) | |
,fp(NULL) | |
,encoder(NULL) | |
,sws(NULL) | |
,num_nals(0) | |
,pts(0) | |
{ | |
memset((char*)&pic_raw, 0, sizeof(pic_raw)); | |
} | |
X264Encoder::~X264Encoder() { | |
if(sws) { | |
close(); | |
} | |
} | |
bool X264Encoder::open(std::string filename, bool datapath) { | |
if(!validateSettings()) { | |
return false; | |
} | |
int r = 0; | |
int nheader = 0; | |
int header_size = 0; | |
// @todo add validate which checks if all params are set (in/out width/height, fps,etc..); | |
if(encoder) { | |
RX_ERROR("Already opened. first call close()"); | |
return false; | |
} | |
if(out_pixel_format != AV_PIX_FMT_YUV420P) { | |
RX_ERROR("At this moment the output format must be AV_PIX_FMT_YUV420P"); | |
return false; | |
} | |
sws = sws_getContext(in_width, in_height, in_pixel_format, | |
out_width, out_height, out_pixel_format, | |
SWS_FAST_BILINEAR, NULL, NULL, NULL); | |
if(!sws) { | |
RX_ERROR("Cannot create SWS context"); | |
::exit(EXIT_FAILURE); | |
} | |
if(datapath) { | |
filename = rx_to_data_path(filename); | |
} | |
fp = fopen(filename.c_str(), "w+b"); | |
if(!fp) { | |
RX_ERROR("Cannot open the h264 destination file"); | |
goto error; | |
} | |
x264_picture_alloc(&pic_in, X264_CSP_I420, out_width, out_height); | |
setParams(); | |
// create the encoder using our params | |
encoder = x264_encoder_open(¶ms); | |
if(!encoder) { | |
RX_ERROR("Cannot open the encoder"); | |
goto error; | |
} | |
// write headers | |
r = x264_encoder_headers(encoder, &nals, &nheader); | |
if(r < 0) { | |
RX_ERROR("x264_encoder_headers() failed"); | |
goto error; | |
} | |
header_size = nals[0].i_payload + nals[1].i_payload +nals[2].i_payload; | |
if(!fwrite(nals[0].p_payload, header_size, 1, fp)) { | |
RX_ERROR("Cannot write headers"); | |
goto error; | |
} | |
pts = 0; | |
return true; | |
error: | |
close(); | |
return false; | |
} | |
bool X264Encoder::encode(char* pixels) { | |
if(!sws) { | |
RX_ERROR("Not initialized, so cannot encode"); | |
return false; | |
} | |
// copy the pixels into our "raw input" container. | |
int bytes_filled = avpicture_fill(&pic_raw, (uint8_t*)pixels, in_pixel_format, in_width, in_height); | |
if(!bytes_filled) { | |
RX_ERROR("Cannot fill the raw input buffer"); | |
return false; | |
} | |
// convert to I420 for x264 | |
int h = sws_scale(sws, pic_raw.data, pic_raw.linesize, 0, | |
in_height, pic_in.img.plane, pic_in.img.i_stride); | |
if(h != out_height) { | |
RX_ERROR("scale failed: %d", h); | |
return false; | |
} | |
// and encode and store into pic_out | |
pic_in.i_pts = pts; | |
int frame_size = x264_encoder_encode(encoder, &nals, &num_nals, &pic_in, &pic_out); | |
if(frame_size) { | |
if(!fwrite(nals[0].p_payload, frame_size, 1, fp)) { | |
RX_ERROR("Error while trying to write nal"); | |
return false; | |
} | |
} | |
++pts; | |
return true; | |
} | |
bool X264Encoder::close() { | |
if(encoder) { | |
x264_picture_clean(&pic_in); | |
memset((char*)&pic_in, 0, sizeof(pic_in)); | |
memset((char*)&pic_out, 0, sizeof(pic_out)); | |
x264_encoder_close(encoder); | |
encoder = NULL; | |
} | |
if(sws) { | |
sws_freeContext(sws); | |
sws = NULL; | |
} | |
memset((char*)&pic_raw, 0, sizeof(pic_raw)); | |
if(fp) { | |
fclose(fp); | |
fp = NULL; | |
} | |
return true; | |
} | |
void X264Encoder::setParams() { | |
x264_param_default_preset(¶ms, "ultrafast", "zerolatency"); | |
params.i_threads = 1; | |
params.i_width = out_width; | |
params.i_height = out_height; | |
params.i_fps_num = fps; | |
params.i_fps_den = 1; | |
} | |
bool X264Encoder::validateSettings() { | |
if(!in_width) { | |
RX_ERROR("No in_width set"); | |
return false; | |
} | |
if(!in_height) { | |
RX_ERROR("No in_height set"); | |
return false; | |
} | |
if(!out_width) { | |
RX_ERROR("No out_width set"); | |
return false; | |
} | |
if(!out_height) { | |
RX_ERROR("No out_height set"); | |
return false; | |
} | |
if(in_pixel_format == AV_PIX_FMT_NONE) { | |
RX_ERROR("No in_pixel_format set"); | |
return false; | |
} | |
if(out_pixel_format == AV_PIX_FMT_NONE) { | |
RX_ERROR("No out_pixel_format set"); | |
return false; | |
} | |
return true; | |
} | |
/* | |
# X264Encoder | |
Simple wrapper for x264 that you can use to encode into x264. Make | |
sure to set all the params before calling `open()`. See below for the | |
members that the you need to set. | |
*/ | |
#ifndef ROXLU_X264_ENCODER_H | |
#define ROXLU_X264_ENCODER_H | |
#include <inttypes.h> | |
#include <stdio.h> | |
#include <string> | |
extern "C" { | |
# include <x264.h> | |
# include <libswscale/swscale.h> | |
# include <libavcodec/avcodec.h> | |
} | |
class X264Encoder { | |
public: | |
X264Encoder(); | |
~X264Encoder(); | |
bool open(std::string filename, bool datapath); /* open for encoding */ | |
bool encode(char* pixels); /* encode the given data */ | |
bool close(); /* close the encoder and file, frees all memory */ | |
private: | |
bool validateSettings(); /* validates if all params are set correctly, like width,height, etc.. */ | |
void setParams(); /* sets the x264 params */ | |
public: | |
/* params the user should set */ | |
int in_width; | |
int in_height; | |
int out_width; | |
int out_height; | |
int fps; /* e.g. 25, 60, etc.. */ | |
AVPixelFormat in_pixel_format; | |
AVPixelFormat out_pixel_format; | |
/* x264 */ | |
AVPicture pic_raw; /* used for our "raw" input container */ | |
x264_picture_t pic_in; | |
x264_picture_t pic_out; | |
x264_param_t params; | |
x264_nal_t* nals; | |
x264_t* encoder; | |
int num_nals; | |
/* input / output */ | |
int pts; | |
struct SwsContext* sws; | |
FILE* fp; | |
}; | |
#endif |
#include <video/X264EncoderThreaded.h> | |
// ---------------------------------------------------------------- | |
void x264_encoder_thread(void* user) { | |
X264EncoderThreaded& enc_threaded = *(static_cast<X264EncoderThreaded*>(user)); | |
X264Encoder& enc = enc_threaded.enc; | |
char* frame = NULL; | |
AVPicture pic; | |
int num_bytes = avpicture_fill(&pic, NULL, enc.in_pixel_format, enc.in_width, enc.in_height); | |
frame = new char[num_bytes * 2]; // lib swscale needs some more space | |
std::vector<X264Data> work_data; | |
while(!enc_threaded.must_stop) { | |
// get data we need to encode | |
enc_threaded.lock(); | |
{ | |
std::copy(enc_threaded.input_data.begin(), enc_threaded.input_data.end(), std::back_inserter(work_data)); | |
enc_threaded.input_data.clear(); | |
} | |
enc_threaded.unlock(); | |
// And encode | |
for(std::vector<X264Data>::iterator it = work_data.begin(); it != work_data.end(); ++it) { | |
X264Data& data = *it; | |
enc_threaded.buffer.read(frame, data.nbytes); | |
enc.encode(frame); | |
} | |
work_data.clear(); | |
} | |
enc.close(); | |
delete[] frame; | |
frame = NULL; | |
} | |
// ---------------------------------------------------------------- | |
X264EncoderThreaded::X264EncoderThreaded() | |
:must_stop(false) | |
,buffer(1024 * 1024 * 512) /* 300 mb, must be enough for quite some frames */ | |
,num_bytes(0) | |
{ | |
uv_mutex_init(&mutex); | |
} | |
X264EncoderThreaded::~X264EncoderThreaded() { | |
stop(); | |
uv_thread_join(&thread_id); | |
uv_mutex_destroy(&mutex); | |
} | |
bool X264EncoderThreaded::start(std::string filename, bool datapath) { | |
if(!enc.open(filename, datapath)) { | |
return false; | |
} | |
// get the size for one frame. | |
AVPicture pic; | |
num_bytes = avpicture_fill(&pic, NULL, enc.in_pixel_format, enc.in_width, enc.in_height); | |
uv_thread_create(&thread_id, x264_encoder_thread, this); | |
return true; | |
} | |
void X264EncoderThreaded::encode(char* pixels) { | |
X264Data data; | |
data.offset = buffer.getWriteIndex(); | |
data.nbytes = num_bytes; | |
size_t written = buffer.write((const char*)pixels, num_bytes); | |
lock(); | |
input_data.push_back(data); | |
unlock(); | |
//RX_VERBOSE("WRITTEN %ld, write index: %ld", written, buffer.getWriteIndex()); | |
} |
#ifndef ROXLU_X264_ENCODER_THREADED_H | |
#define ROXLU_X264_ENCODER_THREADED_H | |
extern "C" { | |
# include <uv.h> | |
} | |
#include <string> | |
#include <vector> | |
#include <roxlu/Roxlu.h> /* we need the RingBuffer */ | |
#include <video/X264Encoder.h> | |
void x264_encoder_thread(void* user); | |
struct X264Data { | |
size_t offset; | |
size_t nbytes; | |
}; | |
class X264EncoderThreaded { | |
public: | |
X264EncoderThreaded(); | |
~X264EncoderThreaded(); | |
bool start(std::string filename, bool datapath); | |
void encode(char* pixels); | |
void stop(); | |
void lock(); | |
void unlock(); | |
public: | |
RingBuffer buffer; | |
X264Encoder enc; /* the encoder we use to encode to h264 */ | |
uv_thread_t thread_id; | |
uv_mutex_t mutex; | |
size_t num_bytes; /* num bytes per frame */ | |
volatile bool must_stop; | |
std::vector<X264Data> input_data; | |
}; | |
inline void X264EncoderThreaded::lock() { | |
uv_mutex_lock(&mutex); | |
} | |
inline void X264EncoderThreaded::unlock() { | |
uv_mutex_unlock(&mutex); | |
} | |
inline void X264EncoderThreaded::stop() { | |
must_stop = true; | |
} | |
#endif |
#include <video/YUV420P.h> | |
YUV420P::YUV420P() { | |
memset(linesize, 0, sizeof(linesize)); | |
memset(width, 0, sizeof(width)); | |
memset(height, 0, sizeof(height)); | |
} | |
YUV420P::~YUV420P() { | |
} | |
//bool YUV420P::setup(VideoBuffer* vb) { | |
//bool YUV420P::setup(AVDecoderFrame* f) { | |
bool YUV420P::setup(AVFrame* ff) { | |
if(!ff) { | |
RX_ERROR("Invalid AVFrame"); | |
return false; | |
} | |
// setup width/height/linesizes | |
width[0] = ff->width; // Y | |
width[1] = ff->width * 0.5; // U | |
width[2] = ff->width * 0.5; // V | |
height[0] = ff->height; // Y | |
height[1] = ff->height * 0.5; // U | |
height[2] = ff->height * 0.5; // V | |
linesize[0] = ff->linesize[0]; // Y | |
linesize[1] = ff->linesize[1]; // U | |
linesize[2] = ff->linesize[2]; // V | |
// validate | |
if(!width[0] || !width[1] || !width[2]) { | |
RX_ERROR("Cannot validate width"); | |
return false; | |
} | |
if(!height[0] || !height[1] || !height[2]) { | |
RX_ERROR("Cannot validate height"); | |
return false; | |
} | |
if(!linesize[0] || !linesize[1] || !linesize[2]) { | |
RX_ERROR("Cannot validate linesize"); | |
return false; | |
} | |
// Setup video textures | |
if(!vt_y.setup(width[0], height[0], GL_R8, GL_RED, GL_UNSIGNED_BYTE, linesize[0])) { | |
RX_ERROR("Cannot setup the VideoTexture for the Y plane"); | |
return false; | |
} | |
if(!vt_u.setup(width[1], height[1], GL_R8, GL_RED, GL_UNSIGNED_BYTE, linesize[1])) { | |
RX_ERROR("Cannot setup the VideoTexture for the U plane"); | |
return false; | |
} | |
if(!vt_v.setup(width[2], height[2], GL_R8, GL_RED, GL_UNSIGNED_BYTE, linesize[2])) { | |
RX_ERROR("Cannot setup the VideoTexture for the V plane"); | |
return false; | |
} | |
print(); | |
return true; | |
} | |
bool YUV420P::updateTextures(AVFrame* ff) { | |
if(!ff) { | |
RX_ERROR("Invalid AVFrame"); | |
return false; | |
} | |
if(!ff->width || !ff->height) { | |
RX_ERROR("Invalid width / height for the given AVDecoderFrame: %d x %d", ff->width, ff->height); | |
return false; | |
} | |
vt_y.setPixels((char*)ff->data[0]); | |
vt_u.setPixels((char*)ff->data[1]); | |
vt_v.setPixels((char*)ff->data[2]); | |
return true; | |
} | |
void YUV420P::print() { | |
RX_VERBOSE("yuv, plane 0: width: %d, height: %d, stride: %d", width[0], height[0], linesize[0]); | |
RX_VERBOSE("yuv, plane 1: width: %d, height: %d, stride: %d", width[1], height[1], linesize[1]); | |
RX_VERBOSE("yuv, plane 2: width: %d, height: %d, stride: %d", width[2], height[2], linesize[2]); | |
} |
/* | |
# YUV420P | |
This class is a basic container for YUV420P based video data. | |
We assume the given VideoBuffer contains AVDecoderFrames/AVFrames | |
of the type AV_PIX_FMT_YUV420P. | |
This classes uses 3 fast VideoTextures to stream video data to the | |
GPU (using round robbin', orphaned pbos) | |
*/ | |
#ifndef ROXLU_YUV420P_H | |
#define ROXLU_YUV420P_H | |
#include <roxlu/core/Log.h> | |
#include <roxlu/opengl/GL.h> | |
#include <av/AVDecoder.h> | |
#include <video/VideoTexture.h> | |
class YUV420P { | |
public: | |
YUV420P(); | |
~YUV420P(); | |
bool setup(AVFrame* f); | |
void print(); /* print debug info */ | |
bool updateTextures(AVFrame* f); | |
GLuint getYTexture(); | |
GLuint getUTexture(); | |
GLuint getVTexture(); | |
public: | |
int linesize[3]; /* strides of the 3 planes */ | |
int width[3]; /* widths of the 3 planes */ | |
int height[3]; /* heigts of the 3 planes */ | |
VideoTexture vt_y; | |
VideoTexture vt_u; | |
VideoTexture vt_v; | |
}; | |
inline GLuint YUV420P::getYTexture() { | |
return vt_y.getTexture(); | |
} | |
inline GLuint YUV420P::getUTexture() { | |
return vt_u.getTexture(); | |
} | |
inline GLuint YUV420P::getVTexture() { | |
return vt_v.getTexture(); | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment