Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/lightnvr.ini
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ max_size = 0 ; 0 means unlimited, otherwise bytes
retention_days = 30
auto_delete_oldest = true

; MP4 faststart: rewrite each finished segment with its index (moov) at the
; front. The rewrite re-reads and re-writes the whole segment, roughly tripling
; the I/O on NFS/SMB storage or a slow HDD. Range-request players (browsers,
; the web UI) play either layout. Set false when recording to a NAS.
mp4_faststart = true

; Capacity-based retention: keep at least this percent of the volume free by
; auto-evicting the oldest recordings, regardless of retention_days. This bounds
; usage to the disk size so the volume never fills. Set to 0 to disable.
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ mp4_retention_days = 30
camera was permanently deleted; per-camera overrides still apply, and
protected recordings are never expired by age.
- `auto_delete_oldest`: Whether to automatically delete the oldest recordings when storage is full
- `mp4_faststart`: Whether to rewrite each finished MP4 segment with its index (moov) at the front (default `true`). The rewrite re-reads and re-writes the whole segment, which roughly triples I/O on network storage (NFS/SMB) or a slow HDD; players that use HTTP range requests play either layout, so set `false` when recording to a NAS.
- `record_mp4_directly`: Enable direct MP4 recording (instead of HLS-to-MP4 conversion)
- `mp4_path`: Directory for direct MP4 recordings
- `mp4_directory_format`: Recording folder layout: `flat`, `year_month` (`YYYY/MM`), or `year_month_day` (`YYYY/MM/DD`, default)
Expand Down
6 changes: 6 additions & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ typedef struct {
uint64_t max_storage_size; // in bytes
int retention_days;
bool auto_delete_oldest;
// MP4 "faststart": after a segment closes, rewrite it with the moov atom
// moved to the front. That re-reads and re-writes the whole segment, which
// roughly triples the I/O on network storage (NFS/SMB) or a slow HDD.
// Players that use HTTP range requests play either layout, so a site
// recording to a NAS may turn it off. Default true (historical behaviour).
bool mp4_faststart;

// Capacity-based retention (bounds usage by disk size, not just by time).
// The cleanup thread keeps at least storage_min_free_pct of the volume free
Expand Down
6 changes: 6 additions & 0 deletions src/core/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ void load_default_config(config_t *config) {
config->max_storage_size = 0; // 0 means unlimited
config->retention_days = 30;
config->auto_delete_oldest = true;
config->mp4_faststart = true;

// Capacity-based retention: keep 10% of the volume free by default so the
// disk is self-bounding even when retention_days exceeds what fits.
Expand Down Expand Up @@ -814,6 +815,8 @@ static int config_ini_handler(void* user, const char* section, const char* name,
config->retention_days = safe_atoi(value, 0);
} else if (strcmp(name, "auto_delete_oldest") == 0) {
config->auto_delete_oldest = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(name, "mp4_faststart") == 0) {
config->mp4_faststart = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(name, "min_free_pct") == 0) {
config->storage_min_free_pct = safe_atoi(value, 10);
} else if (strcmp(name, "pressure_warning_pct") == 0) {
Expand Down Expand Up @@ -1803,6 +1806,8 @@ int save_config(const config_t *config, const char *path) {
fprintf(file, "max_size = %llu ; 0 means unlimited, otherwise bytes\n", (unsigned long long)config->max_storage_size);
fprintf(file, "retention_days = %d\n", config->retention_days);
fprintf(file, "auto_delete_oldest = %s\n", config->auto_delete_oldest ? "true" : "false");
fprintf(file, "mp4_faststart = %s ; false avoids the post-segment rewrite (less I/O on NAS/HDD)\n",
config->mp4_faststart ? "true" : "false");
fprintf(file, "min_free_pct = %d ; keep at least this %% of the volume free (0 disables capacity cap)\n",
config->storage_min_free_pct);
fprintf(file, "pressure_warning_pct = %.1f\n", config->storage_pressure_warning_pct);
Expand Down Expand Up @@ -2012,6 +2017,7 @@ void print_config(const config_t *config) {
printf(" Max Storage Size: %llu bytes\n", (unsigned long long)config->max_storage_size);
printf(" Retention Days: %d\n", config->retention_days);
printf(" Auto Delete Oldest: %s\n", config->auto_delete_oldest ? "true" : "false");
printf(" MP4 Faststart: %s\n", config->mp4_faststart ? "true" : "false");

printf(" Health Settings:\n");
printf(" Enabled: %s\n", config->health.enabled ? "true" : "false");
Expand Down
8 changes: 6 additions & 2 deletions src/video/mp4_segment_recorder.c
Original file line number Diff line number Diff line change
Expand Up @@ -847,8 +847,12 @@ int record_segment(const char *rtsp_url, const char *output_file, int duration,

// Use faststart to move moov atom to beginning for better compatibility
// This creates standard MP4 files that play in all applications
// The + prefix adds to existing flags rather than replacing them
av_dict_set(&out_opts, "movflags", "+faststart", 0);
// The + prefix adds to existing flags rather than replacing them.
// [storage] mp4_faststart = false skips it: the rewrite re-reads and
// re-writes the whole segment, which triples the I/O on a NAS.
if (g_config.mp4_faststart) {
av_dict_set(&out_opts, "movflags", "+faststart", 0);
}

// CRITICAL FIX: Validate output_file parameter before attempting to open
if (!output_file || output_file[0] == '\0') {
Expand Down
4 changes: 3 additions & 1 deletion src/video/mp4_writer_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -1595,7 +1595,9 @@ int mp4_writer_initialize(mp4_writer_t *writer, const AVPacket *pkt, const AVStr

// Set options for fast start - EXACTLY match rtsp_recorder.c
AVDictionary *opts = NULL;
av_dict_set(&opts, "movflags", "+faststart", 0); // This is the ONLY option in rtsp_recorder.c
if (g_config.mp4_faststart) { // [storage] mp4_faststart (default true)
av_dict_set(&opts, "movflags", "+faststart", 0); // This is the ONLY option in rtsp_recorder.c
}

// Open output file
ret = avio_open(&writer->output_ctx->pb, writer->output_path, AVIO_FLAG_WRITE);
Expand Down
9 changes: 9 additions & 0 deletions src/web/api_handlers_settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ void handle_get_settings(const http_request_t *req, http_response_t *res) {
cJSON_AddNumberToObject(settings, "lpr_retention_days", days);
}
cJSON_AddBoolToObject(settings, "auto_delete_oldest", g_config.auto_delete_oldest);
cJSON_AddBoolToObject(settings, "mp4_faststart", g_config.mp4_faststart);
cJSON_AddNumberToObject(settings, "storage_min_free_pct", g_config.storage_min_free_pct);
cJSON_AddNumberToObject(settings, "storage_pressure_warning_pct", g_config.storage_pressure_warning_pct);
cJSON_AddNumberToObject(settings, "storage_pressure_critical_pct", g_config.storage_pressure_critical_pct);
Expand Down Expand Up @@ -1338,6 +1339,14 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) {
log_info("Updated auto_delete_oldest: %s", g_config.auto_delete_oldest ? "true" : "false");
}

// MP4 faststart (applies to segments opened after the change)
cJSON *mp4_faststart = cJSON_GetObjectItem(settings, "mp4_faststart");
if (mp4_faststart && cJSON_IsBool(mp4_faststart)) {
g_config.mp4_faststart = cJSON_IsTrue(mp4_faststart);
settings_changed = true;
log_info("Updated mp4_faststart: %s", g_config.mp4_faststart ? "true" : "false");
}

// Capacity-based retention: minimum free-space headroom to maintain (%)
cJSON *min_free_pct = cJSON_GetObjectItem(settings, "storage_min_free_pct");
if (min_free_pct && cJSON_IsNumber(min_free_pct)) {
Expand Down
39 changes: 38 additions & 1 deletion tests/unit/test_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,39 @@ void test_default_config_retention_days(void) {
TEST_ASSERT_EQUAL_INT(30, cfg.retention_days);
}

void test_default_config_mp4_faststart_on(void) {
load_default_config(&cfg);
TEST_ASSERT_TRUE(cfg.mp4_faststart);
}

/* save_config must write mp4_faststart, or a settings save from the web UI
* would silently turn faststart back on at the next start. */
void test_save_config_round_trips_mp4_faststart(void) {
char temp_dir[] = "/tmp/lightnvr_faststart_XXXXXX";
char *dir = mkdtemp(temp_dir);
TEST_ASSERT_NOT_NULL(dir);
char config_path[MAX_PATH_LENGTH];
snprintf(config_path, sizeof(config_path), "%s/faststart.ini", dir);

load_default_config(&cfg);
cfg.mp4_faststart = false;
TEST_ASSERT_EQUAL_INT(0, save_config(&cfg, config_path));

FILE *saved = fopen(config_path, "r");
TEST_ASSERT_NOT_NULL(saved);
char line[512];
bool found = false;
while (fgets(line, sizeof(line), saved)) {
if (strncmp(line, "mp4_faststart = false", 21) == 0) {
found = true;
}
}
fclose(saved);
unlink(config_path);
rmdir(dir);
TEST_ASSERT_TRUE(found);
}

void test_default_config_buffer_size(void) {
load_default_config(&cfg);
TEST_ASSERT_GREATER_THAN_INT(0, cfg.buffer_size);
Expand Down Expand Up @@ -636,7 +669,8 @@ void test_env_integer_whitespace_handling(void) {
"[storage]\n"
"path = %s\n"
"path_hls = %s\n"
"mp4_directory_format = year_month\n\n"
"mp4_directory_format = year_month\n"
"mp4_faststart = false\n\n"
"[models]\n"
"path = %s\n\n"
"[database]\n"
Expand Down Expand Up @@ -686,6 +720,7 @@ void test_env_integer_whitespace_handling(void) {
TEST_ASSERT_EQUAL_INT(12, cfg.db_backup_retention_count);
TEST_ASSERT_EQUAL_STRING("/usr/local/bin/backup-hook", cfg.db_post_backup_script);
TEST_ASSERT_EQUAL_STRING("year_month", cfg.mp4_directory_format);
TEST_ASSERT_FALSE(cfg.mp4_faststart);
TEST_ASSERT_TRUE(cfg.audio_disabled);
TEST_ASSERT_TRUE(cfg.auto_disabled);
TEST_ASSERT_EQUAL_STRING("conservative", cfg.health.profile);
Expand Down Expand Up @@ -748,6 +783,8 @@ int main(void) {
RUN_TEST(test_default_config_web_port);
RUN_TEST(test_default_config_log_level);
RUN_TEST(test_default_config_retention_days);
RUN_TEST(test_default_config_mp4_faststart_on);
RUN_TEST(test_save_config_round_trips_mp4_faststart);
RUN_TEST(test_default_config_buffer_size);
RUN_TEST(test_default_config_storage_path_nonempty);
RUN_TEST(test_default_config_db_path_nonempty);
Expand Down
Loading