diff --git a/frontend/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui index a1f6e6320e1199..914535999ae52a 100644 --- a/frontend/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -5818,7 +5818,7 @@ 20 - 8192 + 8388608 512 @@ -5839,6 +5839,27 @@ + + + + Storage Mode (RAM / Disk) + + + + + + + + RAM (Memory) + + + + + SSD/HDD (Disk) + + + + diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp index cace1de59db0e9..0791351eda4a3c 100644 --- a/frontend/settings/OBSBasicSettings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -520,6 +520,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->advOutTrack6Name, EDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advReplayBuf, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advRBSecMax, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advRBStorageMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advRBMegsMax, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->channelSetup, COMBO_CHANGED, AUDIO_RESTART); HookWidget(ui->sampleRate, COMBO_CHANGED, AUDIO_RESTART); @@ -805,6 +806,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) connect(ui->advOutRecType, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); connect(ui->advOutRecEncoder, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); connect(ui->advRBSecMax, &QSpinBox::valueChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); + connect(ui->advRBStorageMode, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::AdvReplayBufferChanged); // GPU scaling filters auto addScaleFilter = [&](const char *string, int value) -> void { @@ -2615,6 +2617,7 @@ void OBSBasicSettings::LoadAdvancedSettings() bool replayBuf = config_get_bool(main->Config(), "AdvOut", "RecRB"); int rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); int rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + int rbStorageMode = config_get_int(main->Config(), "AdvOut", "RecRBStorageMode"); bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); const char *hotkeyFocusType = config_get_string(App()->GetUserConfig(), "General", "HotkeyFocusType"); bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); @@ -2639,6 +2642,7 @@ void OBSBasicSettings::LoadAdvancedSettings() ui->advReplayBuf->setChecked(replayBuf); ui->advRBSecMax->setValue(rbTime); ui->advRBMegsMax->setValue(rbSize); + ui->advRBStorageMode->setCurrentIndex(rbStorageMode); ui->reconnectEnable->setChecked(reconnect); ui->reconnectRetryDelay->setValue(retryDelay); @@ -3602,6 +3606,9 @@ void OBSBasicSettings::SaveOutputSettings() SaveCheckBox(ui->advReplayBuf, "AdvOut", "RecRB"); SaveSpinBox(ui->advRBSecMax, "AdvOut", "RecRBTime"); SaveSpinBox(ui->advRBMegsMax, "AdvOut", "RecRBSize"); + if (WidgetChanged(ui->advRBStorageMode)) { + config_set_int(main->Config(), "AdvOut", "RecRBStorageMode", ui->advRBStorageMode->currentIndex()); + } WriteJsonData(streamEncoderProps, "streamEncoder.json"); WriteJsonData(recordEncoderProps, "recordEncoder.json"); @@ -5298,9 +5305,14 @@ void OBSBasicSettings::AdvReplayBufferChanged() int seconds = ui->advRBSecMax->value(); - // Set maximum to 75% of installed memory - uint64_t memTotal = os_get_sys_total_size(); - int64_t memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; + int64_t memMaxMB; + if (ui->advRBStorageMode->currentIndex() == 1) { + memMaxMB = 8388608; // 8TB limit for Disk + } else { + // Set maximum to 75% of installed memory for RAM + uint64_t memTotal = os_get_sys_total_size(); + memMaxMB = memTotal ? memTotal * 3 / 4 / 1024 / 1024 : 8192; + } int64_t memMB = int64_t(seconds) * int64_t(vbitrate + abitrate) * 1000 / 8 / 1024 / 1024; if (memMB < 1) { diff --git a/frontend/utility/AdvancedOutput.cpp b/frontend/utility/AdvancedOutput.cpp index 5adac3f0faa1c6..dd9a79cc384535 100644 --- a/frontend/utility/AdvancedOutput.cpp +++ b/frontend/utility/AdvancedOutput.cpp @@ -906,6 +906,7 @@ bool AdvancedOutput::StartReplayBuffer() rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + int rbStorageMode = config_get_int(main->Config(), "AdvOut", "RecRBStorageMode"); string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); string ext = GetFormatExt(recFormat); @@ -918,6 +919,7 @@ bool AdvancedOutput::StartReplayBuffer() obs_data_set_bool(settings, "allow_spaces", !noSpace); obs_data_set_int(settings, "max_time_sec", rbTime); obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + obs_data_set_int(settings, "storage_mode", rbStorageMode); obs_output_update(replayBuffer, settings); } diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index f152b2849e3e1a..a62e887f66d5eb 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -47,9 +47,9 @@ static const char *ffmpeg_mpegts_mux_getname(void *type) static inline void replay_buffer_clear(struct ffmpeg_muxer *stream) { while (stream->packets.size > 0) { - struct encoder_packet pkt; - deque_pop_front(&stream->packets, &pkt, sizeof(pkt)); - obs_encoder_packet_release(&pkt); + struct rb_packet rb_pkt; + deque_pop_front(&stream->packets, &rb_pkt, sizeof(rb_pkt)); + obs_encoder_packet_release(&rb_pkt.pkt); } deque_free(&stream->packets); @@ -69,7 +69,7 @@ static void ffmpeg_mux_destroy(void *data) if (stream->mux_thread_joinable) pthread_join(stream->mux_thread, NULL); for (size_t i = 0; i < stream->mux_packets.num; i++) - obs_encoder_packet_release(&stream->mux_packets.array[i]); + obs_encoder_packet_release(&stream->mux_packets.array[i].pkt); da_free(stream->mux_packets); deque_free(&stream->packets); @@ -766,9 +766,10 @@ static inline bool has_audio(struct ffmpeg_muxer *stream) static void push_back_packet(mux_packets_t *packets, struct encoder_packet *packet) { - struct encoder_packet pkt; - obs_encoder_packet_ref(&pkt, packet); - da_push_back(*packets, &pkt); + struct rb_packet rb_pkt; + obs_encoder_packet_ref(&rb_pkt.pkt, packet); + rb_pkt.disk_offset = -1; + da_push_back(*packets, &rb_pkt); } static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) @@ -786,7 +787,8 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) if (stream->split_file && stream->mux_packets.num) { int64_t pts_usec = packet_pts_usec(packet); - struct encoder_packet *first_pkt = stream->mux_packets.array; + struct rb_packet *first_rb = &stream->mux_packets.array[0]; + struct encoder_packet *first_pkt = &first_rb->pkt; int64_t first_pts_usec = packet_pts_usec(first_pkt); if (pts_usec >= first_pts_usec) { @@ -829,7 +831,8 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) if (stream->split_file && stream->split_file_ready) { for (size_t i = 0; i < stream->mux_packets.num; i++) { - struct encoder_packet *pkt = &stream->mux_packets.array[i]; + struct rb_packet *rb = &stream->mux_packets.array[i]; + struct encoder_packet *pkt = &rb->pkt; ts_offset_update(stream, pkt); write_packet(stream, pkt); obs_encoder_packet_release(pkt); @@ -972,6 +975,12 @@ static void replay_buffer_destroy(void *data) struct ffmpeg_muxer *stream = data; if (stream->hotkey) obs_hotkey_unregister(stream->hotkey); + if (stream->disk_tmp_file) { + fclose(stream->disk_tmp_file); + stream->disk_tmp_file = NULL; + os_unlink(stream->disk_tmp_path.array); + dstr_free(&stream->disk_tmp_path); + } ffmpeg_mux_destroy(data); } @@ -987,6 +996,27 @@ static bool replay_buffer_start(void *data) obs_data_t *s = obs_output_get_settings(stream->output); stream->max_time = obs_data_get_int(s, "max_time_sec") * 1000000LL; stream->max_size = obs_data_get_int(s, "max_size_mb") * (1024 * 1024); + stream->storage_mode = (int)obs_data_get_int(s, "storage_mode"); + + if (stream->storage_mode == 1) { + const char *dir = obs_data_get_string(s, "directory"); + dstr_copy(&stream->disk_tmp_path, dir); + char slash = '/'; + if (stream->disk_tmp_path.array && stream->disk_tmp_path.array[stream->disk_tmp_path.len - 1] == '\\') { + slash = '\\'; + } + if (stream->disk_tmp_path.array && stream->disk_tmp_path.array[stream->disk_tmp_path.len - 1] != slash) { + dstr_cat_ch(&stream->disk_tmp_path, slash); + } + dstr_cat(&stream->disk_tmp_path, "replay_buffer_disk.tmp"); + stream->disk_tmp_file = os_fopen(stream->disk_tmp_path.array, "wb+"); + stream->disk_write_pos = 0; + if (!stream->disk_tmp_file) { + warn("Failed to create temporary disk replay buffer file '%s'", stream->disk_tmp_path.array); + stream->storage_mode = 0; // Fallback to RAM + } + } + obs_data_release(s); os_atomic_set_bool(&stream->active, true); @@ -999,15 +1029,16 @@ static bool replay_buffer_start(void *data) static bool purge_front(struct ffmpeg_muxer *stream) { - struct encoder_packet pkt; + struct rb_packet rb_pkt; bool keyframe; if (!stream->packets.size) return false; - deque_pop_front(&stream->packets, &pkt, sizeof(pkt)); + deque_pop_front(&stream->packets, &rb_pkt, sizeof(rb_pkt)); + struct encoder_packet *pkt = &rb_pkt.pkt; - keyframe = pkt.type == OBS_ENCODER_VIDEO && pkt.keyframe; + keyframe = pkt->type == OBS_ENCODER_VIDEO && pkt->keyframe; if (keyframe) stream->keyframes--; @@ -1016,26 +1047,26 @@ static bool purge_front(struct ffmpeg_muxer *stream) stream->cur_size = 0; stream->cur_time = 0; } else { - struct encoder_packet first; + struct rb_packet first; deque_peek_front(&stream->packets, &first, sizeof(first)); - stream->cur_time = first.dts_usec; - stream->cur_size -= (int64_t)pkt.size; + stream->cur_time = first.pkt.dts_usec; + stream->cur_size -= (int64_t)pkt->size; } - obs_encoder_packet_release(&pkt); + obs_encoder_packet_release(pkt); return keyframe; } static inline void purge(struct ffmpeg_muxer *stream) { if (purge_front(stream)) { - struct encoder_packet pkt; + struct rb_packet rb_pkt; for (;;) { if (!stream->packets.size) return; - deque_peek_front(&stream->packets, &pkt, sizeof(pkt)); - if (pkt.type == OBS_ENCODER_VIDEO && pkt.keyframe) + deque_peek_front(&stream->packets, &rb_pkt, sizeof(rb_pkt)); + if (rb_pkt.pkt.type == OBS_ENCODER_VIDEO && rb_pkt.pkt.keyframe) return; purge_front(stream); @@ -1060,37 +1091,46 @@ static inline void replay_buffer_purge(struct ffmpeg_muxer *stream, struct encod purge(stream); } -static void insert_packet(mux_packets_t *packets, struct encoder_packet *packet, int64_t video_offset, +static void insert_packet(mux_packets_t *packets, struct rb_packet *packet, int64_t video_offset, int64_t *audio_offsets, int64_t video_pts_offset, int64_t *audio_dts_offsets) { - struct encoder_packet pkt; + struct rb_packet rb_pkt = *packet; + struct encoder_packet *pkt = &rb_pkt.pkt; size_t idx; - obs_encoder_packet_ref(&pkt, packet); + obs_encoder_packet_ref(pkt, &packet->pkt); - if (pkt.type == OBS_ENCODER_VIDEO) { - pkt.dts_usec -= video_offset; - pkt.dts -= video_pts_offset; - pkt.pts -= video_pts_offset; + if (pkt->type == OBS_ENCODER_VIDEO) { + pkt->dts_usec -= video_offset; + pkt->dts -= video_pts_offset; + pkt->pts -= video_pts_offset; } else { - pkt.dts_usec -= audio_offsets[pkt.track_idx]; - pkt.dts -= audio_dts_offsets[pkt.track_idx]; - pkt.pts -= audio_dts_offsets[pkt.track_idx]; + pkt->dts_usec -= audio_offsets[pkt->track_idx]; + pkt->dts -= audio_dts_offsets[pkt->track_idx]; + pkt->pts -= audio_dts_offsets[pkt->track_idx]; } for (idx = packets->num; idx > 0; idx--) { - struct encoder_packet *p = packets->array + (idx - 1); - if (p->dts_usec < pkt.dts_usec) + struct rb_packet *p = packets->array + (idx - 1); + if (p->pkt.dts_usec < pkt->dts_usec) break; } - da_insert(*packets, idx, &pkt); + da_insert(*packets, idx, &rb_pkt); } static void *replay_buffer_mux_thread(void *data) { struct ffmpeg_muxer *stream = data; bool error = false; + + FILE *disk_read_file = NULL; + if (stream->storage_mode == 1 && stream->disk_tmp_path.array) { + disk_read_file = os_fopen(stream->disk_tmp_path.array, "rb"); + if (!disk_read_file) { + warn("Failed to open temporary disk replay buffer file for reading '%s'", stream->disk_tmp_path.array); + } + } start_pipe(stream, stream->path.array); @@ -1107,7 +1147,21 @@ static void *replay_buffer_mux_thread(void *data) } for (size_t i = 0; i < stream->mux_packets.num; i++) { - struct encoder_packet *pkt = &stream->mux_packets.array[i]; + struct rb_packet *rb = &stream->mux_packets.array[i]; + struct encoder_packet *pkt = &rb->pkt; + + // Load from disk if needed + if (stream->storage_mode == 1 && rb->disk_offset >= 0 && disk_read_file) { + long *p_refs = bmalloc(pkt->size + sizeof(long)); + *p_refs = 1; + pkt->data = (void *)(p_refs + 1); + os_fseeki64(disk_read_file, rb->disk_offset, SEEK_SET); + size_t read_bytes = fread(pkt->data, 1, pkt->size, disk_read_file); + if (read_bytes != pkt->size) { + warn("Failed to read complete packet from disk"); + } + } + if (!write_packet(stream, pkt)) { warn("Could not write packet for file '%s'", stream->path.array); error = true; @@ -1119,11 +1173,14 @@ static void *replay_buffer_mux_thread(void *data) info("Wrote replay buffer to '%s'", stream->path.array); error: + if (disk_read_file) { + fclose(disk_read_file); + } os_process_pipe_destroy(stream->pipe); stream->pipe = NULL; if (error) { for (size_t i = 0; i < stream->mux_packets.num; i++) - obs_encoder_packet_release(&stream->mux_packets.array[i]); + obs_encoder_packet_release(&stream->mux_packets.array[i].pkt); } da_free(stream->mux_packets); os_atomic_set_bool(&stream->muxing, false); @@ -1139,7 +1196,7 @@ static void *replay_buffer_mux_thread(void *data) static void replay_buffer_save(struct ffmpeg_muxer *stream) { - const size_t size = sizeof(struct encoder_packet); + const size_t size = sizeof(struct rb_packet); size_t num_packets = stream->packets.size / size; da_reserve(stream->mux_packets, num_packets); @@ -1155,8 +1212,8 @@ static void replay_buffer_save(struct ffmpeg_muxer *stream) int64_t audio_dts_offsets[MAX_AUDIO_MIXES] = {0}; for (size_t i = 0; i < num_packets; i++) { - struct encoder_packet *pkt; - pkt = deque_data(&stream->packets, i * size); + struct rb_packet *rb = deque_data(&stream->packets, i * size); + struct encoder_packet *pkt = &rb->pkt; if (pkt->type == OBS_ENCODER_VIDEO) { if (!found_video) { @@ -1172,7 +1229,7 @@ static void replay_buffer_save(struct ffmpeg_muxer *stream) } } - insert_packet(&stream->mux_packets, pkt, video_offset, audio_offsets, video_pts_offset, + insert_packet(&stream->mux_packets, rb, video_offset, audio_offsets, video_pts_offset, audio_dts_offsets); } @@ -1198,6 +1255,13 @@ static void deactivate_replay_buffer(struct ffmpeg_muxer *stream, int code) os_atomic_set_bool(&stream->sent_headers, false); os_atomic_set_bool(&stream->stopping, false); replay_buffer_clear(stream); + + if (stream->disk_tmp_file) { + fclose(stream->disk_tmp_file); + stream->disk_tmp_file = NULL; + os_unlink(stream->disk_tmp_path.array); + dstr_free(&stream->disk_tmp_path); + } } static void replay_buffer_data(void *data, struct encoder_packet *packet) @@ -1222,13 +1286,49 @@ static void replay_buffer_data(void *data, struct encoder_packet *packet) } obs_encoder_packet_ref(&pkt, packet); + + struct rb_packet rb_pkt; + rb_pkt.pkt = pkt; + rb_pkt.disk_offset = -1; + + if (stream->storage_mode == 1 && stream->disk_tmp_file && pkt.data && pkt.size > 0) { + // Calculate wrap-around if needed + int64_t disk_size_limit = stream->max_size > 0 ? stream->max_size : (int64_t)8LL * 1024 * 1024 * 1024; // 8GB default limit + if (stream->disk_write_pos + (int64_t)pkt.size > disk_size_limit) { + stream->disk_write_pos = 0; // Wrap around + os_fseeki64(stream->disk_tmp_file, 0, SEEK_SET); + } + + // Purge packets that are about to be overwritten + while (stream->packets.size > 0) { + struct rb_packet first; + deque_peek_front(&stream->packets, &first, sizeof(first)); + if (first.disk_offset != -1 && first.disk_offset >= stream->disk_write_pos && + first.disk_offset < stream->disk_write_pos + (int64_t)pkt.size) { + purge_front(stream); + } else { + break; + } + } + + rb_pkt.disk_offset = stream->disk_write_pos; + fwrite(pkt.data, 1, pkt.size, stream->disk_tmp_file); + stream->disk_write_pos += pkt.size; + + // Free actual data from RAM but keep the structure + long *p_refs = ((long *)pkt.data) - 1; + if (os_atomic_dec_long(p_refs) == 0) + bfree(p_refs); + rb_pkt.pkt.data = NULL; + } + replay_buffer_purge(stream, &pkt); if (!stream->packets.size) stream->cur_time = pkt.dts_usec; stream->cur_size += pkt.size; - deque_push_back(&stream->packets, packet, sizeof(*packet)); + deque_push_back(&stream->packets, &rb_pkt, sizeof(rb_pkt)); if (packet->type == OBS_ENCODER_VIDEO && packet->keyframe) stream->keyframes++; diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h index 077031d07403b1..6387356c1e1e66 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h @@ -9,7 +9,12 @@ #include #include -typedef DARRAY(struct encoder_packet) mux_packets_t; +struct rb_packet { + struct encoder_packet pkt; + int64_t disk_offset; +}; + +typedef DARRAY(struct rb_packet) mux_packets_t; struct ffmpeg_muxer { obs_output_t *output; @@ -32,6 +37,11 @@ struct ffmpeg_muxer { int64_t max_time; /* replay buffer */ + int storage_mode; + FILE *disk_tmp_file; + struct dstr disk_tmp_path; + int64_t disk_write_pos; + int64_t save_ts; int keyframes; obs_hotkey_id hotkey;