Previously, the design for the interaction between the encoder thread and the graphics thread was that the encoder thread would signal to the graphics thread when to start drawing each frame. The original idea behind this was to prevent mutually cascading stalls of encoding or graphics rendering (i.e., if rendering took too long, then encoding would have to catch up, then rendering would have to catch up again, and so on, cascading upon each other). The ultimate goal was to prevent encoding from impacting graphics and vise versa. However, eventually it was realized that there were some fundamental flaws with this design. 1. Stray frame duplication. You could not guarantee that a frame would render on time, so sometimes frames would unintentionally be lost if there was any sort of minor hiccup or if the thread took too long to be scheduled I'm guessing. 2. Frame timing in the rendering thread was less accurate. The only place where frame timing was accurate was in the encoder thread, and the graphics thread was at the whim of thread scheduling. On higher end computers it was typically fine, but it was just generally not guaranteed that a frame would be rendered when it was supposed to be rendered. So the solution (originally proposed by r1ch and paibox) is to instead keep the encoding and graphics threads separate as usual, but instead of the encoder thread controlling the graphics thread, the graphics thread now controls the encoder thread. The encoder thread keeps a limited cache of frames, then the graphics thread copies frames in to the cache and increments a semaphore to schedule the encoder thread to encode that data. In the cache, each frame has an encode counter. If the frame cache is full (e.g., the encoder taking too long to return frames), it will not cache a new frame, but instead will just increment the counter on the last frame in the cache to schedule that frame to encode again, ensuring that frames are on time and reducing CPU usage by lowering video complexity. If the graphics thread takes too long to render a frame, then it will add that frame with the count value set to the total amount of frames that were missed (actual legitimately duplicated frames). Because the cache gives many frames of breathing room for the encoder to encode frames, this design helps improve results especially when using encoding presets that have higher complexity and CPU usage, minimizing the risk of needlessly skipped or duplicated frames. I also managed to sneak in what should be a bit of an optimization to reduce copying of frame data, though how much of an optimization it ultimately ends up being is debatable. So to sum it up, this commit increases accuracy of frame timing, completely removes stray frame duplication, gives better results for higher complexity encoding presets, and potentially optimizes the frame pipeline a tiny bit.
489 lines
12 KiB
C
489 lines
12 KiB
C
/******************************************************************************
|
|
Copyright (C) 2013 by Hugh Bailey <obs.jim@gmail.com>
|
|
|
|
This program 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 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include <assert.h>
|
|
#include "../util/bmem.h"
|
|
#include "../util/platform.h"
|
|
#include "../util/threading.h"
|
|
#include "../util/darray.h"
|
|
|
|
#include "format-conversion.h"
|
|
#include "video-io.h"
|
|
#include "video-frame.h"
|
|
#include "video-scaler.h"
|
|
|
|
#define MAX_CONVERT_BUFFERS 3
|
|
#define MAX_CACHE_SIZE 16
|
|
|
|
struct cached_frame_info {
|
|
struct video_data frame;
|
|
int count;
|
|
};
|
|
|
|
struct video_input {
|
|
struct video_scale_info conversion;
|
|
video_scaler_t *scaler;
|
|
struct video_frame frame[MAX_CONVERT_BUFFERS];
|
|
int cur_frame;
|
|
|
|
void (*callback)(void *param, struct video_data *frame);
|
|
void *param;
|
|
};
|
|
|
|
static inline void video_input_free(struct video_input *input)
|
|
{
|
|
for (size_t i = 0; i < MAX_CONVERT_BUFFERS; i++)
|
|
video_frame_free(&input->frame[i]);
|
|
video_scaler_destroy(input->scaler);
|
|
}
|
|
|
|
struct video_output {
|
|
struct video_output_info info;
|
|
|
|
pthread_t thread;
|
|
pthread_mutex_t data_mutex;
|
|
bool stop;
|
|
|
|
os_sem_t *update_semaphore;
|
|
uint64_t frame_time;
|
|
uint32_t skipped_frames;
|
|
uint32_t total_frames;
|
|
|
|
bool initialized;
|
|
|
|
pthread_mutex_t input_mutex;
|
|
DARRAY(struct video_input) inputs;
|
|
|
|
size_t available_frames;
|
|
size_t first_added;
|
|
size_t last_added;
|
|
struct cached_frame_info cache[MAX_CACHE_SIZE];
|
|
};
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static inline bool scale_video_output(struct video_input *input,
|
|
struct video_data *data)
|
|
{
|
|
bool success = true;
|
|
|
|
if (input->scaler) {
|
|
struct video_frame *frame;
|
|
|
|
if (++input->cur_frame == MAX_CONVERT_BUFFERS)
|
|
input->cur_frame = 0;
|
|
|
|
frame = &input->frame[input->cur_frame];
|
|
|
|
success = video_scaler_scale(input->scaler,
|
|
frame->data, frame->linesize,
|
|
(const uint8_t * const*)data->data,
|
|
data->linesize);
|
|
|
|
if (success) {
|
|
for (size_t i = 0; i < MAX_AV_PLANES; i++) {
|
|
data->data[i] = frame->data[i];
|
|
data->linesize[i] = frame->linesize[i];
|
|
}
|
|
} else {
|
|
blog(LOG_WARNING, "video-io: Could not scale frame!");
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static inline bool video_output_cur_frame(struct video_output *video)
|
|
{
|
|
struct cached_frame_info *frame_info;
|
|
bool complete;
|
|
|
|
/* -------------------------------- */
|
|
|
|
pthread_mutex_lock(&video->data_mutex);
|
|
|
|
frame_info = &video->cache[video->first_added];
|
|
|
|
pthread_mutex_unlock(&video->data_mutex);
|
|
|
|
/* -------------------------------- */
|
|
|
|
pthread_mutex_lock(&video->input_mutex);
|
|
|
|
for (size_t i = 0; i < video->inputs.num; i++) {
|
|
struct video_input *input = video->inputs.array+i;
|
|
struct video_data frame = frame_info->frame;
|
|
|
|
if (scale_video_output(input, &frame))
|
|
input->callback(input->param, &frame);
|
|
}
|
|
|
|
pthread_mutex_unlock(&video->input_mutex);
|
|
|
|
/* -------------------------------- */
|
|
|
|
pthread_mutex_lock(&video->data_mutex);
|
|
|
|
frame_info->frame.timestamp += video->frame_time;
|
|
complete = --frame_info->count == 0;
|
|
|
|
if (complete) {
|
|
if (++video->first_added == video->info.cache_size)
|
|
video->first_added = 0;
|
|
|
|
if (++video->available_frames == video->info.cache_size)
|
|
video->last_added = video->first_added;
|
|
}
|
|
|
|
pthread_mutex_unlock(&video->data_mutex);
|
|
|
|
/* -------------------------------- */
|
|
|
|
return complete;
|
|
}
|
|
|
|
static void *video_thread(void *param)
|
|
{
|
|
struct video_output *video = param;
|
|
|
|
while (os_sem_wait(video->update_semaphore) == 0) {
|
|
if (video->stop)
|
|
break;
|
|
|
|
while (!video->stop && !video_output_cur_frame(video)) {
|
|
video->total_frames++;
|
|
video->skipped_frames++;
|
|
}
|
|
|
|
video->total_frames++;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static inline bool valid_video_params(const struct video_output_info *info)
|
|
{
|
|
return info->height != 0 && info->width != 0 && info->fps_den != 0 &&
|
|
info->fps_num != 0;
|
|
}
|
|
|
|
static inline void init_cache(struct video_output *video)
|
|
{
|
|
if (video->info.cache_size > MAX_CACHE_SIZE)
|
|
video->info.cache_size = MAX_CACHE_SIZE;
|
|
|
|
for (size_t i = 0; i < video->info.cache_size; i++) {
|
|
struct video_frame *frame;
|
|
frame = (struct video_frame*)&video->cache[i];
|
|
|
|
video_frame_init(frame, video->info.format,
|
|
video->info.width, video->info.height);
|
|
}
|
|
|
|
video->available_frames = video->info.cache_size;
|
|
}
|
|
|
|
int video_output_open(video_t **video, struct video_output_info *info)
|
|
{
|
|
struct video_output *out;
|
|
pthread_mutexattr_t attr;
|
|
|
|
if (!valid_video_params(info))
|
|
return VIDEO_OUTPUT_INVALIDPARAM;
|
|
|
|
out = bzalloc(sizeof(struct video_output));
|
|
if (!out)
|
|
goto fail;
|
|
|
|
memcpy(&out->info, info, sizeof(struct video_output_info));
|
|
out->frame_time = (uint64_t)(1000000000.0 * (double)info->fps_den /
|
|
(double)info->fps_num);
|
|
out->initialized = false;
|
|
|
|
if (pthread_mutexattr_init(&attr) != 0)
|
|
goto fail;
|
|
if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0)
|
|
goto fail;
|
|
if (pthread_mutex_init(&out->data_mutex, &attr) != 0)
|
|
goto fail;
|
|
if (pthread_mutex_init(&out->input_mutex, &attr) != 0)
|
|
goto fail;
|
|
if (os_sem_init(&out->update_semaphore, 0) != 0)
|
|
goto fail;
|
|
if (pthread_create(&out->thread, NULL, video_thread, out) != 0)
|
|
goto fail;
|
|
|
|
init_cache(out);
|
|
|
|
out->initialized = true;
|
|
*video = out;
|
|
return VIDEO_OUTPUT_SUCCESS;
|
|
|
|
fail:
|
|
video_output_close(out);
|
|
return VIDEO_OUTPUT_FAIL;
|
|
}
|
|
|
|
void video_output_close(video_t *video)
|
|
{
|
|
if (!video)
|
|
return;
|
|
|
|
video_output_stop(video);
|
|
|
|
for (size_t i = 0; i < video->inputs.num; i++)
|
|
video_input_free(&video->inputs.array[i]);
|
|
da_free(video->inputs);
|
|
|
|
for (size_t i = 0; i < video->info.cache_size; i++)
|
|
video_frame_free((struct video_frame*)&video->cache[i]);
|
|
|
|
os_sem_destroy(video->update_semaphore);
|
|
pthread_mutex_destroy(&video->data_mutex);
|
|
pthread_mutex_destroy(&video->input_mutex);
|
|
bfree(video);
|
|
}
|
|
|
|
static size_t video_get_input_idx(const video_t *video,
|
|
void (*callback)(void *param, struct video_data *frame),
|
|
void *param)
|
|
{
|
|
for (size_t i = 0; i < video->inputs.num; i++) {
|
|
struct video_input *input = video->inputs.array+i;
|
|
if (input->callback == callback && input->param == param)
|
|
return i;
|
|
}
|
|
|
|
return DARRAY_INVALID;
|
|
}
|
|
|
|
static inline bool video_input_init(struct video_input *input,
|
|
struct video_output *video)
|
|
{
|
|
if (input->conversion.width != video->info.width ||
|
|
input->conversion.height != video->info.height ||
|
|
input->conversion.format != video->info.format) {
|
|
struct video_scale_info from = {
|
|
.format = video->info.format,
|
|
.width = video->info.width,
|
|
.height = video->info.height,
|
|
};
|
|
|
|
int ret = video_scaler_create(&input->scaler,
|
|
&input->conversion, &from,
|
|
VIDEO_SCALE_FAST_BILINEAR);
|
|
if (ret != VIDEO_SCALER_SUCCESS) {
|
|
if (ret == VIDEO_SCALER_BAD_CONVERSION)
|
|
blog(LOG_ERROR, "video_input_init: Bad "
|
|
"scale conversion type");
|
|
else
|
|
blog(LOG_ERROR, "video_input_init: Failed to "
|
|
"create scaler");
|
|
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < MAX_CONVERT_BUFFERS; i++)
|
|
video_frame_init(&input->frame[i],
|
|
input->conversion.format,
|
|
input->conversion.width,
|
|
input->conversion.height);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool video_output_connect(video_t *video,
|
|
const struct video_scale_info *conversion,
|
|
void (*callback)(void *param, struct video_data *frame),
|
|
void *param)
|
|
{
|
|
bool success = false;
|
|
|
|
if (!video || !callback)
|
|
return false;
|
|
|
|
pthread_mutex_lock(&video->input_mutex);
|
|
|
|
if (video_get_input_idx(video, callback, param) == DARRAY_INVALID) {
|
|
struct video_input input;
|
|
memset(&input, 0, sizeof(input));
|
|
|
|
input.callback = callback;
|
|
input.param = param;
|
|
|
|
if (conversion) {
|
|
input.conversion = *conversion;
|
|
} else {
|
|
input.conversion.format = video->info.format;
|
|
input.conversion.width = video->info.width;
|
|
input.conversion.height = video->info.height;
|
|
}
|
|
|
|
if (input.conversion.width == 0)
|
|
input.conversion.width = video->info.width;
|
|
if (input.conversion.height == 0)
|
|
input.conversion.height = video->info.height;
|
|
|
|
success = video_input_init(&input, video);
|
|
if (success)
|
|
da_push_back(video->inputs, &input);
|
|
}
|
|
|
|
pthread_mutex_unlock(&video->input_mutex);
|
|
|
|
return success;
|
|
}
|
|
|
|
void video_output_disconnect(video_t *video,
|
|
void (*callback)(void *param, struct video_data *frame),
|
|
void *param)
|
|
{
|
|
if (!video || !callback)
|
|
return;
|
|
|
|
pthread_mutex_lock(&video->input_mutex);
|
|
|
|
size_t idx = video_get_input_idx(video, callback, param);
|
|
if (idx != DARRAY_INVALID) {
|
|
video_input_free(video->inputs.array+idx);
|
|
da_erase(video->inputs, idx);
|
|
}
|
|
|
|
pthread_mutex_unlock(&video->input_mutex);
|
|
}
|
|
|
|
bool video_output_active(const video_t *video)
|
|
{
|
|
if (!video) return false;
|
|
return video->inputs.num != 0;
|
|
}
|
|
|
|
const struct video_output_info *video_output_get_info(const video_t *video)
|
|
{
|
|
return video ? &video->info : NULL;
|
|
}
|
|
|
|
bool video_output_lock_frame(video_t *video, struct video_frame *frame,
|
|
int count, uint64_t timestamp)
|
|
{
|
|
struct cached_frame_info *cfi;
|
|
bool locked;
|
|
|
|
if (!video) return false;
|
|
|
|
pthread_mutex_lock(&video->data_mutex);
|
|
|
|
if (video->available_frames == 0) {
|
|
video->cache[video->last_added].count += count;
|
|
locked = false;
|
|
|
|
} else {
|
|
if (video->available_frames != video->info.cache_size) {
|
|
if (++video->last_added == video->info.cache_size)
|
|
video->last_added = 0;
|
|
}
|
|
|
|
cfi = &video->cache[video->last_added];
|
|
cfi->frame.timestamp = timestamp;
|
|
cfi->count = count;
|
|
|
|
memcpy(frame, &cfi->frame, sizeof(*frame));
|
|
|
|
locked = true;
|
|
}
|
|
|
|
pthread_mutex_unlock(&video->data_mutex);
|
|
|
|
return locked;
|
|
}
|
|
|
|
void video_output_unlock_frame(video_t *video)
|
|
{
|
|
if (!video) return;
|
|
|
|
pthread_mutex_lock(&video->data_mutex);
|
|
|
|
video->available_frames--;
|
|
os_sem_post(video->update_semaphore);
|
|
|
|
pthread_mutex_unlock(&video->data_mutex);
|
|
}
|
|
|
|
uint64_t video_output_get_frame_time(const video_t *video)
|
|
{
|
|
return video ? video->frame_time : 0;
|
|
}
|
|
|
|
void video_output_stop(video_t *video)
|
|
{
|
|
void *thread_ret;
|
|
|
|
if (!video)
|
|
return;
|
|
|
|
if (video->initialized) {
|
|
video->initialized = false;
|
|
video->stop = true;
|
|
os_sem_post(video->update_semaphore);
|
|
pthread_join(video->thread, &thread_ret);
|
|
}
|
|
}
|
|
|
|
bool video_output_stopped(video_t *video)
|
|
{
|
|
if (!video)
|
|
return true;
|
|
|
|
return video->stop;
|
|
}
|
|
|
|
enum video_format video_output_get_format(const video_t *video)
|
|
{
|
|
return video ? video->info.format : VIDEO_FORMAT_NONE;
|
|
}
|
|
|
|
uint32_t video_output_get_width(const video_t *video)
|
|
{
|
|
return video ? video->info.width : 0;
|
|
}
|
|
|
|
uint32_t video_output_get_height(const video_t *video)
|
|
{
|
|
return video ? video->info.height : 0;
|
|
}
|
|
|
|
double video_output_get_frame_rate(const video_t *video)
|
|
{
|
|
if (!video)
|
|
return 0.0;
|
|
|
|
return (double)video->info.fps_num / (double)video->info.fps_den;
|
|
}
|
|
|
|
uint32_t video_output_get_skipped_frames(const video_t *video)
|
|
{
|
|
return video->skipped_frames;
|
|
}
|
|
|
|
uint32_t video_output_get_total_frames(const video_t *video)
|
|
{
|
|
return video->total_frames;
|
|
}
|