Commit Graph
457 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 4.8 eb83ca1e78 fix: return the insert id as uint64_t so event ids past 2^31 survive
mysql_insert_id() returns my_ulonglong, but zmDbDoInsert() returned int.
Once Events.Id passed INT32_MAX the id was truncated, then sign-extended
when stored in Event::id (uint64_t), so 3502167073 became
18446744072916751373. The Events_Hour/Day/Week/Month inserts were given
the negative value and rejected, and every later UPDATE of the event
matched no row.

Also widen the callers that re-truncated the result: the re-analysis
event id in zma, Tag::Id() when building an Event_Tag, and
Event_Tag::save(). The Events_* insert format specifiers become PRIu64
to match the unsigned id.

fixes #4974

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV1gDc9T6D6H8gtTcxwLvk
2026-07-21 18:01:05 -04:00
Isaac Connor 346fa0e8a0 Improve error message when we fail to write capture jpg 2026-07-01 11:52:25 -04:00
Isaac Connor 60060a5d9f fix: close DB/rename race that exposed incomplete.* to event consumers
On event close ~Event() renamed the video file on disk synchronously but
queued the DefaultVideo=final DB update on the async dbQueue. The monitor's
close_event_thread forks EventEndCommand as soon as ~Event returns, so an
external consumer (e.g. AI analysis via the CakePHP API) could read the
stale DefaultVideo='incomplete.<codec>.<container>' after the file had
already been renamed, producing "File does not exist" errors.

Hard-link the file to its final <Id>-video.* name instead of renaming, so
it resolves under both names for the whole window the DB may still advertise
the old name. Write the finalized Events row synchronously (close runs in
its own thread, so this no longer blocks the analysis thread) and only then
unlink the incomplete.* name, once DefaultVideo=final is committed.

Dedupe by inode in the DiskSpace sum so the doubly-named file is not counted
twice, and guard the m3u8 URL rewrite against the link-failure fallback
where video_file == video_incomplete_file, which would loop forever.
2026-06-19 10:42:18 -04:00
Isaac Connor a380f4a83e fix: keep VideoStore path in sync when incomplete file is renamed
During recording the incomplete video file is renamed to embed the codec
in its name (incomplete.<codec>.<container>) so canPlayCodec() works. The
VideoStore had opened the file under the old name, so FFmpeg's faststart
trailer re-open (oc->url) and the mfra read in finalize() could fail with
ENOENT after the rename.

Add VideoStore::set_filename() to update filename and oc->url, and call it
after the rename. Also warn when movflags contains faststart, which is
incompatible with the fragmented MP4 / HLS output this class produces.
2026-06-19 09:52:36 -04:00
Isaac ConnorandClaude Opus 4.8 a36375585c refactor: make zm_terminate a std::atomic<bool>
zm_terminate is written from signal handlers and read from daemon loops.
A plain bool is undefined to access from a signal handler and may be hoisted
out of a loop by the optimizer. std::atomic<bool> is lock-free on supported
platforms and matches the already-atomic per-object terminate_ flags. Update
the three Debug() vararg sites to call .load() since atomics can't pass
through ...

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:11:51 -04:00
Isaac ConnorandClaude Opus 4.7 db6747402e fix: keep DefaultVideo as mp4 name throughout event lifecycle refs #4839
DefaultVideo was being set to 'index.m3u8' at INSERT when the video
writer was enabled, then overwritten with the codec-bearing mp4 name
at event close. That left a window during recording where consumers
saw 'index.m3u8' instead of a real mp4 filename, and forced every
consumer (view_video.php, image.php, event.php) to special-case the
'index.m3u8' string.

Now:
  - INSERT leaves DefaultVideo empty.
  - After videoStore->open() succeeds, UPDATE DefaultVideo to the
    codec-bearing incomplete.{codec}.{container} name. If the rename
    to add the codec failed, fall back to the plain incomplete name
    so DefaultVideo always matches what is actually on disk.
  - Close-time UPDATE (existing) sets the final {id}-video.{codec}.
    {container}.

Invariant for consumers: non-empty DefaultVideo => a playable mp4 is
being written or has been written. The codec is encoded in the name
for the entire recording lifetime.

event.php: drop the str_ends_with(DefaultVideo, '.m3u8') branch from
$video_tag (codec detection on the next line already covers in-progress
events now that DefaultVideo carries the codec). Drop the
DefaultVideo === 'index.m3u8' download-tooltip special case. Remove
the stale comment about the constructor-time 'index.m3u8' value.

The === 'index.m3u8' checks in view_video.php and image.php are left
in place as belt-and-braces for any legacy DB rows recorded under the
old behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:24:40 -04:00
Isaac ConnorandClaude Opus 4.7 2e83b8b074 revert: stop forcing DefaultVideo='index.m3u8' for HLS events refs #4757
47d3af70c rewrote DefaultVideo to 'index.m3u8' on close so event.php
would pick HLS for closed events. But several PHP call sites use
DefaultVideo as a local file path passed to ffmpeg or to filesize() —
Event.php's getImageSrc(), Length(), Filesize(), and Video() all rely
on it being a real video filename. ffmpeg can't resolve our manifest's
relative "index.php?…" segment URLs, so thumbnail/frame generation
would break for events with SaveJPEGs disabled.

The PHP-side fallback (10a4f9af1, 375e82fd8) keys HLS selection off the
on-disk index.m3u8 plus the codec choice, which is enough on its own to
route closed events through HLS without touching DefaultVideo.

Reported by Copilot on PR #4835.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:01:19 -04:00
Isaac ConnorandClaude Opus 4.7 47d3af70c7 fix: keep DefaultVideo='index.m3u8' for events with an HLS manifest refs #4757
Event::~Event() rewrote DefaultVideo to the renamed mp4 filename on every
close, which made event.php's HLS detection (str_ends_with DefaultVideo,
'.m3u8') return false for every closed event. The HLS player only kicked
in for in-progress events or when MP4HLS was manually picked from the
codec dropdown.

Detect index.m3u8 in the same dirent walk that already computes video_size,
and write 'index.m3u8' to DefaultVideo when it's present. mp4-only events
keep using the renamed mp4 as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:54:32 -04:00
Isaac ConnorandClaude Opus 4.7 0e532cd77b fix: correct HLS fragment byte-range and duration tracking refs #4757
The m3u8 manifest written by VideoStore had two interlocking bugs that
produced duplicate entries with bogus 0.040s durations and a missing or
mis-pointed final fragment, which made video.js seek backwards.

Root cause: with movflags=frag_keyframe the mov muxer flushes fragment N
to disk *inside* av_interleaved_write_frame() when keyframe N+1 arrives.
The previous code snapshotted avio_tell() *before* that call, so its
last_fragment_offset_ described the next fragment's start while the
fragment at that offset hadn't actually been flushed yet. writeM3U8
then push_back'd a tentative entry off that stale state on every live
update, and the next keyframe push pushed a second entry at the same
offset+size with the correct duration.

Now we snapshot avio_tell() *after* av_interleaved_write_frame(), which
gives the actual end of the just-flushed fragment, and record fragment
N-1 there. writeM3U8 no longer mutates fragments_ — it just emits the
list.

The final fragment (no later keyframe to close it) is recorded by a new
finalize() method on VideoStore that runs av_interleaved_write_frame
flush + av_write_trailer, then parses the trailing mfro box to subtract
the mfra trailer size from the file length. Event::~Event() calls
finalize() before writeM3U8(true); the VideoStore destructor skips its
own trailer write when finalize() has already run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:28:00 -04:00
Isaac ConnorandClaude Opus 4.6 3f8bc81760 fix: HLS fragment tracking, live playback, and fallback handling
Fragment tracking:
- Rewrite detection to use avio_flush()+avio_tell() before each video
  keyframe write, giving exact fragment boundaries. Fixes duplicate
  entries and missing first fragment in the m3u8 manifest.
- Remove unused pending_fragment_dts_ and last_flush_pos_ members.

Live event playback:
- Set DefaultVideo to index.m3u8 at event INSERT time (no DB update
  needed after videoStore opens)
- Rename incomplete file to include codec (incomplete.h264.mp4) so
  canPlayCodec() works during recording
- Rewrite final m3u8 after video file rename to reference final name
- Add live retry handler in event.php: retries all error codes with
  backoff (3s then 5s), max 30 retries, resets on successful playback
- Update progress bar duration from video element for live events

Fallback handling:
- view_video.php: when DefaultVideo is m3u8 and no file param given,
  search event dir for actual mp4 (final name then incomplete)
- view_hls.php: reject m3u8 with no EXTINF entries (event just started)
- event.php: require m3u8 file to exist on disk before offering HLS,
  fall back to direct MP4 otherwise

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:57:46 -04:00
Isaac ConnorandClaude Opus 4.6 e15d2a6427 feat: add HLS byte-range playback for event video
Write a single continuous fragmented MP4 per event and generate an HLS
m3u8 manifest with byte-range references into that file. This enables
seamless browser playback via video.js's built-in http-streaming (VHS)
without needing separate segment files.

C++ changes:
- VideoStore tracks fragment boundaries (moof+mdat byte offsets and
  durations) as packets are written, by monitoring avio_tell() around
  keyframe writes in write_packet()
- Add writeM3U8() method that generates EXT-X-VERSION:7 byte-range
  manifests with EXT-X-MAP for the init segment
- Event writes a live m3u8 (no EXT-X-ENDLIST) on each new fragment
  for in-progress viewing, and a final VOD manifest at event close
- Change movflags to frag_keyframe+empty_moov+default_base_moof
  (default_base_moof required by HLS fMP4 spec, faststart removed
  as it's meaningless with empty_moov)

PHP/web changes:
- New view_hls.php endpoint serves pre-built m3u8 with auth tokens
- event.php detects index.m3u8 and uses HLS as primary source with
  direct MP4 as fallback
- CSRF exemption for view_hls in index.php

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:40:12 -04:00
Isaac ConnorandClaude Opus 4.6 0882a3ad1e fix: resolve Event::Run thread hang preventing zmc clean shutdown
Event::Run could block indefinitely in PacketQueue methods during normal
event closing (closeEvent from analysis thread), because their wait
predicates only check deleting/zm_terminate, not Event's terminate_ flag.

Three changes fix this:
- get_packet_no_wait: return immediately when iterator at end instead of
  blocking on condition variable (makes it truly non-blocking)
- Event::Run: use increment_it(wait=false) since deletePacket can advance
  the iterator to end() during AddPacket_ without the queue lock
- Event::Stop: call packetqueue->notify_all() to wake timed waits so
  Run() checks terminate_ promptly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:00:59 -05:00
Isaac ConnorandClaude Opus 4.6 d5b4709584 fix: reduce packet queue backpressure from event thread blocking
Three changes to prevent the analysis thread from stalling and the
packet queue from filling up:

1. Replace blind sleep_for/usleep in Event::Run() with
   packetqueue->wait_for() condition variable waits. The event thread
   now wakes immediately when decoder/analysis completes or new packets
   are queued, instead of always sleeping the full 33ms/10ms.

2. Add missing packetqueue.notify_all() calls after setting
   packet->analyzed (Monitor::Analyse) and packet->decoded
   (DECODING_NONE path in Monitor::Capture) so the event thread's
   condition waits actually get signaled.

3. Replace synchronous zmDbDoUpdate() calls in Event::~Event() with
   async dbQueue.push(). The two Events UPDATE queries (with Name
   fallback logic) are combined into a single query using MySQL IF().
   This eliminates blocking DB I/O from the close_event_thread, which
   the analysis thread joins on the next closeEvent() call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:17:45 -05:00
Isaac Connor cf59a68128 Use starttime for inserting StartDateTime instead of NOW 2026-02-03 12:10:41 -05:00
Isaac ConnorandClaude Opus 4.5 df6c4a4a13 fix: address multiple bugs across core source files
- zm_analysis_thread.cpp, zm_decoder_thread.cpp: Fix potential deadlock
  in Start() by calling Stop() before joining thread
- zm_camera.cpp: Add null check after avformat_alloc_context()
- zm_comms.cpp: Fix memory leak in InetSocket::bind(), fix error message
  typo in deleteReader(), fix clearReaders/clearWriters to recalculate
  mMaxFd properly
- zm_config.cpp: Fix potential buffer underrun when parsing config files,
  fix misplaced SERVER_ID check logic
- zm_db.cpp: Fix logger level not restored on early return in zmDbDo(),
  fix empty string access in DB_HOST parsing
- zm_event.cpp: Fix typo "foudn" -> "found", fix memory leak with Tag
  allocation, fix variable shadowing with video_file
- zm_event_tag.cpp: Fix null dereference when AssignedBy is NULL
- zm_eventstream.cpp: Fix dangling pointer bugs with emplace_back
  (use auto& instead of auto), fix memory leak in loadInitialEventData

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:18:49 -05:00
Isaac ConnorandClaude Opus 4.5 4444491c66 fix: use errno instead of rename() return value for strerror
rename() returns -1 on error, not an errno value. The actual error
code is stored in errno. Using strerror(result) with result=-1 gives
incorrect error messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 20:49:35 -05:00
Isaac Connor 2033b5eb73 Update Storage DiskSpace once event has been finalized. As we decrement DIskSpace on event deletion, we were getting negative DIskSpace in the Storage table. 2026-01-28 15:15:59 -05:00
Isaac Connor 5c402ccab4 Add wait parameter to increment_it. Sometimes we should wait and sometimes not. 2026-01-27 09:30:28 -05:00
Isaac Connor 41251faefc Use SystemTimePointToMysqlString instead of from_unixtimestamp when insert events. Also we don't have Events_Year yet. 2026-01-24 11:17:40 -05:00
Isaac Connor bfb213d7c3 Do what evetns_insert_trigger used to do 2026-01-24 11:17:17 -05:00
Isaac Connor 4db0a431f8 Use packet->analyzed to keep event behind analysis. 2026-01-16 16:02:01 -05:00
Isaac Connor cb09b2201a Rename locked_packet to packet_lock. Don't unlock packet as it crashes 2026-01-15 12:46:24 -05:00
Isaac Connor 131b5caee0 Update to RAII style ZMPacketLock. Rename snap to packet in zm_monitor. Keep hw_frame in packet." 2026-01-15 12:46:23 -05:00
copilot-swe-agent[bot]andSteveGilvarry 2c2047c343 Replace C-style casts with C++ static_cast for intmax_t
Co-authored-by: SteveGilvarry <7613276+SteveGilvarry@users.noreply.github.com>
2026-01-05 13:36:16 +00:00
copilot-swe-agent[bot]andSteveGilvarry 89e0a4918c Fix %ld format specifiers for time_t on ILP32 platforms
Co-authored-by: SteveGilvarry <7613276+SteveGilvarry@users.noreply.github.com>
2026-01-05 10:36:23 +00:00
Isaac Connor ff1242d839 Add reason for failure to rename 2025-09-04 15:23:23 -04:00
Isaac Connor d390369f5e Only do ai detection tagging if we have nlohmann_json 2025-07-19 19:14:42 -04:00
Isaac Connor 97877335db Add MaxScoreFrameId to the events table and automatically update it during event creation so we don't have to look it up later. Finding the frame with the max score is actually a very heavy query, so lets not do it. 2025-07-15 16:17:36 -04:00
Isaac Connor 24e48c5683 Add tags support to events. 2025-07-05 08:09:09 -04:00
Isaac Connor d2514b5e83 a while is better than a for 2024-10-03 14:42:51 -04:00
Isaac Connor 6b44fda57f Don't need to delete packetqueue_it, as free_it does that. 2024-08-20 15:21:29 -04:00
Isaac Connor 912b1818b7 Use get_packet_no_wait and a sleep so that if told to close we give up on getting a packet 2024-08-01 18:01:28 -04:00
Isaac Connor 1ae223bfc1 Don't incremenet event it until we are done with the packet 2024-07-26 10:49:02 -04:00
Isaac Connor 174fb6ccf0 Remove warning about not getting packet lock 2024-07-04 10:24:28 -04:00
Isaac Connor 2755e5044f Don't pass shared_ptr by ref. Defeats the ref counting 2024-06-28 09:40:49 -04:00
Isaac Connor b1d0b57f61 test for zm_terminate in event run loop as well in order to reduce backtraces on segfault 2024-06-27 08:21:51 -04:00
Isaac Connor c24f61e641 Wait for decoded flag so that we stay behind the decoder 2024-06-17 13:40:17 -04:00
Isaac Connor 742f36c58c Redo the event thread. Instead of analysis adding packets to an event specific queue, just pass in the iterator and let the event thread do it's own locking. This allows us to free ram in packet in the event, and not segfault. 2024-06-14 18:34:33 -04:00
Isaac Connor 61c3943331 use RIAA style 2024-06-14 18:34:33 -04:00
Isaac Connor 371b4a9761 Free up raw images once we are done with them in event. 2024-05-22 09:15:57 -04:00
Isaac Connor 03e3d2245d Remove the event summaries update as we havn't killed that trigger yet 2024-05-12 09:04:37 -04:00
Aaron Kling 402393f2ce Set DiskSpace on event end 2024-05-12 08:57:50 -04:00
Aaron Kling c4683d90a9 Format code using astyle google format
Commands used:
astyle --style=google --indent=spaces=2 --keep-one-line-blocks src/*.cpp
astyle --style=google --indent=spaces=2 --keep-one-line-blocks src/*.h
2024-03-26 13:43:58 -05:00
Isaac Connor c5db82c082 Remove TAPE state. 2024-02-26 15:47:39 -05:00
Isaac Connor 63b3f2bd36 Always increment alarm_frames if it is an alarm packet. Doesn't have to have an image. 2024-02-21 14:21:16 -05:00
Alejandro Criado-Pérez 543683aa4c Corrections on cpp and h files 2023-08-13 01:27:19 +02:00
Isaac Connor 33d9a974d5 white space 2023-07-19 12:38:32 -04:00
Isaac Connor 87fafa43a8 Add storing Latitude&Longitude in the event record. 2023-07-09 12:02:07 -04:00
Isaac Connor 9a996cb423 image may have been deleted, but analysis image may still be there. Correct logic so that analysis frame get saved again. 2023-02-19 14:47:43 -05:00
Isaac Connor a0b23388b8 Handle failure to insert into Events table. 2023-02-02 10:03:18 -05:00