Skip to content

Commit

Permalink
encoder/ffmpeg: Add split framerate with integer fractions
Browse files Browse the repository at this point in the history
It seems to be possible to encode with a different framerate than what libOBS is configured for. While technically any framerate appears to be possible, it is currently limited to integer fractions only in order to make the implementation much easier. Integer fractions only require skipping N frames and multiplying the denominator by N, where N is the configured integer. For sanity reasons, the limit of N is currently 10.

This allows power users to split their streaming and recording framerates with relative ease, and opt for things such as:
- 30 FPS (1/4) streaming with 120 FPS (1/1) recording.
- 30 FPS (1/10) streaming with 300 FPS (1/1) recording.
- 30 FPS (1/10) streaming with 100 FPS (1/3) recording.
- and so on.
While some of these combinations are just stupid, they are now available to power users.
  • Loading branch information
Xaymar committed Sep 14, 2022
1 parent 0ba2849 commit ad10fe1
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 2 deletions.
1 change: 1 addition & 0 deletions data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Encoder.FFmpeg.KeyFrames.IntervalType="Interval Type"
Encoder.FFmpeg.KeyFrames.IntervalType.Frames="Frames"
Encoder.FFmpeg.KeyFrames.IntervalType.Seconds="Seconds"
Encoder.FFmpeg.KeyFrames.Interval="Interval"
Encoder.FFmpeg.Framerate="Framerate Override"

# Encoder/FFmpeg/AMF
Encoder.FFmpeg.AMF.Deprecated="This encoder is deprecated and will be removed soon. Users are urged to migrate to the integrated 'AMD HW H.264 (AVC)' or 'AMD HW H.265 (HEVC)' encoder as soon as possible."
Expand Down
42 changes: 40 additions & 2 deletions source/encoders/encoder-ffmpeg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ extern "C" {
#define ST_KEY_FFMPEG_CUSTOMSETTINGS "FFmpeg.CustomSettings"
#define ST_I18N_FFMPEG_THREADS ST_I18N_FFMPEG ".Threads"
#define ST_KEY_FFMPEG_THREADS "FFmpeg.Threads"
#define ST_I18N_FFMPEG_FRAMERATE ST_I18N_FFMPEG ".Framerate"
#define ST_KEY_FFMPEG_FRAMERATE "FFmpeg.Framerate"
#define ST_I18N_FFMPEG_GPU ST_I18N_FFMPEG ".GPU"
#define ST_KEY_FFMPEG_GPU "FFmpeg.GPU"

Expand Down Expand Up @@ -145,6 +147,14 @@ ffmpeg_instance::ffmpeg_instance(obs_data_t* settings, obs_encoder_t* self, bool
initialize_sw(settings);
}

{ // Set up framerate division.
_framerate_divisor = obs_data_get_int(settings, ST_KEY_FFMPEG_FRAMERATE);

_context->ticks_per_frame = 1;
_context->time_base.num *= _framerate_divisor;
_context->framerate.den *= _framerate_divisor;
}

// Update settings
update(settings);

Expand Down Expand Up @@ -263,8 +273,8 @@ bool ffmpeg_instance::update(obs_data_t* settings)
bool is_seconds = (kf_type == 0);

if (is_seconds) {
_context->gop_size = static_cast<int>(obs_data_get_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS)
* (ovi.fps_num / ovi.fps_den));
double framerate = static_cast<double>(ovi.fps_num) / (static_cast<double>(ovi.fps_den) * _framerate_divisor);
_context->gop_size = static_cast<int>(obs_data_get_double(settings, ST_KEY_KEYFRAMES_INTERVAL_SECONDS) * framerate);
} else {
_context->gop_size = static_cast<int>(obs_data_get_int(settings, ST_KEY_KEYFRAMES_INTERVAL_FRAMES));
}
Expand Down Expand Up @@ -377,6 +387,10 @@ bool ffmpeg_instance::encode_audio(struct encoder_frame* frame, struct encoder_p

bool ffmpeg_instance::encode_video(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet)
{
if ((_framerate_divisor > 1) && (frame->pts % _framerate_divisor != 0)) {
return true;
}

std::shared_ptr<AVFrame> vframe = pop_free_frame(); // Retrieve an empty frame.

// Convert frame.
Expand Down Expand Up @@ -413,6 +427,11 @@ bool ffmpeg_instance::encode_video(struct encoder_frame* frame, struct encoder_p
bool ffmpeg_instance::encode_video(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key,
struct encoder_packet* packet, bool* received_packet)
{
if ((_framerate_divisor > 1) && (pts % _framerate_divisor != 0)) {
*next_key = lock_key;
return true;
}

#ifdef D_PLATFORM_WINDOWS
if (handle == GS_INVALID_HANDLE) {
DLOG_ERROR("Received invalid handle.");
Expand Down Expand Up @@ -1142,6 +1161,25 @@ obs_properties_t* ffmpeg_factory::get_properties2(instance_t* data)
auto p = obs_properties_add_int_slider(grp, ST_KEY_FFMPEG_THREADS, D_TRANSLATE(ST_I18N_FFMPEG_THREADS), 0,
static_cast<int64_t>(std::thread::hardware_concurrency()) * 2, 1);
}

{ // Frame Skipping
obs_video_info ovi;
if (!obs_get_video_info(&ovi)) {
throw std::runtime_error("obs_get_video_info failed unexpectedly.");
}

auto p = obs_properties_add_list(grp, ST_KEY_FFMPEG_FRAMERATE, D_TRANSLATE(ST_I18N_FFMPEG_FRAMERATE),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
// For now, an arbitrary limit of 1/10th the Framerate should be fine.
std::vector<char> buf{size_t{256}, 0, std::allocator<char>()};
for (uint32_t divisor = 1; divisor <= 10; divisor++) {
double fps_num = static_cast<double>(ovi.fps_num) / static_cast<double>(divisor);
double fps = fps_num / static_cast<double>(ovi.fps_den);
snprintf(buf.data(), buf.size(), "%8.2f (%" PRIu32 "/%" PRIu32 ")", fps, ovi.fps_num,
ovi.fps_den * divisor);
obs_property_list_add_int(p, buf.data(), divisor);
}
}
};

return props;
Expand Down
1 change: 1 addition & 0 deletions source/encoders/encoder-ffmpeg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ namespace streamfx::encoder::ffmpeg {

std::size_t _lag_in_frames;
std::size_t _sent_frames;
std::size_t _framerate_divisor;

// Extra Data
bool _have_first_frame;
Expand Down

0 comments on commit ad10fe1

Please sign in to comment.