diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp
index fab46ae83..c63dc62f4 100644
--- a/src/zm_eventstream.cpp
+++ b/src/zm_eventstream.cpp
@@ -25,6 +25,7 @@
 #include "zm_sendfile.h"
 #include "zm_signal.h"
 #include "zm_storage.h"
+#include <algorithm>
 #include <arpa/inet.h>
 #include <sys/stat.h>
 
@@ -86,18 +87,42 @@ bool EventStream::seek(SystemTimePoint event_time) {
     return false;
   }
 
-  for (int i=0; i < event_data->frame_count; i++) {
-    //Info( "eft %d > et %d", event_data->frames[i].timestamp, event_time );
-    if (event_data->frames[i].timestamp >= event_time || 
-        event_data->frames[i].timestamp + event_data->frames[i].delta >=event_time) {
-      curr_frame_id = i + 1;
-      curr_stream_time = event_time;
-      Debug(3, "Set curr_stream_time: %.2f, curr_frame_id: %d",
-          FPSeconds(curr_stream_time.time_since_epoch()).count(),
-          curr_frame_id);
-      return true;
+  // Binary search: frames are sorted by timestamp (ascending).
+  // Find the first frame whose timestamp >= event_time.
+  auto it = std::lower_bound(
+    event_data->frames.begin(),
+    event_data->frames.end(),
+    event_time,
+    [](const FrameData &frame, const SystemTimePoint &t) {
+      return frame.timestamp < t;
+    });
+
+  // If event_time is past all frame timestamps (e.g. at event end_time),
+  // step back to the last frame so the delta check below can evaluate it.
+  if (it == event_data->frames.end() && it != event_data->frames.begin()) {
+    --it;
+  }
+
+  // Check if the previous frame's time window covers event_time.
+  // Original logic: timestamp + delta >= event_time allows a frame whose
+  // timestamp is before the target to match if its display window extends
+  // past it.
+  if (it != event_data->frames.begin()) {
+    auto prev = std::prev(it);
+    if (prev->timestamp + prev->delta >= event_time) {
+      it = prev;
     }
-  } // end foreach frame
+  }
+
+  if (it != event_data->frames.end() &&
+      (it->timestamp >= event_time || it->timestamp + it->delta >= event_time)) {
+    curr_frame_id = static_cast<int>(std::distance(event_data->frames.begin(), it)) + 1;
+    curr_stream_time = event_time;
+    Debug(3, "Set curr_stream_time: %.2f, curr_frame_id: %d",
+        FPSeconds(curr_stream_time.time_since_epoch()).count(),
+        curr_frame_id);
+    return true;
+  }
     Warning("Requested an event time less than the start of the event. event_time %" PRIi64 " < start_time %" PRIi64,
         static_cast<int64>(std::chrono::duration_cast<Seconds>(event_time.time_since_epoch()).count()),
         static_cast<int64>(std::chrono::duration_cast<Seconds>(event_data->start_time.time_since_epoch()).count()));
@@ -111,8 +136,8 @@ bool EventStream::loadInitialEventData(
   loadEventData(init_event_id);
 
   if ( init_frame_id ) {
-    if ( init_frame_id >= event_data->frame_count ) {
-      Error("Invalid frame id specified. %d > %d", init_frame_id, event_data->frame_count);
+    if ( init_frame_id < 1 || init_frame_id > (int)event_data->frames.size() ) {
+      Error("Invalid frame id specified. %d not in [1..%zu]", init_frame_id, event_data->frames.size());
       curr_stream_time = event_data->start_time;
       curr_frame_id = 1;
     } else {
@@ -257,7 +282,7 @@ bool EventStream::loadEventData(uint64_t event_id) {
   int last_id = 0;
   SystemTimePoint last_timestamp = event_data->start_time;
   Microseconds last_offset = Seconds(0);
-  const FrameData *last_frame = nullptr;
+  int last_frame_idx = -1;
 
   // Here are the issues: if showing jpegs, need FrameId.
   // Delta is the time since last frame, not since beginning of Event
@@ -277,14 +302,14 @@ bool EventStream::loadEventData(uint64_t event_id) {
     // Fill in data between bulk frames
     if (id_diff > 1) {
       for (int i = last_id + 1; i < id; i++) {
-        auto frame = event_data->frames.emplace_back(
+        auto &frame = event_data->frames.emplace_back(
                        i,
                        last_timestamp + ((i - last_id) * delta),
-                       std::chrono::duration_cast<Microseconds>((last_frame->timestamp - event_data->start_time) + delta),
+                       std::chrono::duration_cast<Microseconds>((event_data->frames[last_frame_idx].timestamp - event_data->start_time) + delta),
                        delta,
                        false
                      );
-        last_frame = &frame;
+        last_frame_idx = event_data->frames.size() - 1;
         Debug(4, "Frame %d %d timestamp (%f s), offset (%f s) delta (%f s), in_db (%d)",
               i, frame.id,
               FPSeconds(frame.timestamp.time_since_epoch()).count(),
@@ -293,8 +318,8 @@ bool EventStream::loadEventData(uint64_t event_id) {
               frame.in_db);
       }
     }
-    auto frame = event_data->frames.emplace_back(id, timestamp, offset, delta, true);
-    last_frame = &frame;
+    auto &frame = event_data->frames.emplace_back(id, timestamp, offset, delta, true);
+    last_frame_idx = event_data->frames.size() - 1;
     last_id = id;
     last_offset = offset;
     last_timestamp = timestamp;
@@ -308,17 +333,17 @@ bool EventStream::loadEventData(uint64_t event_id) {
 
   if (event_data->end_time.time_since_epoch() != Seconds(0) and event_data->duration != Seconds(0) and event_data->frame_count > last_id) {
     Microseconds delta;
-    if (!last_frame) {
+    if (last_frame_idx < 0) {
       // There were no frames in db
       delta = Microseconds( static_cast<int>(1000000 * base_fps / FPSeconds(event_data->duration).count()) );
-      auto frame = event_data->frames.emplace_back(
+      event_data->frames.emplace_back(
                      1,
                      event_data->start_time,
                      Microseconds(0),
                      Microseconds(0),
                      false
                    );
-      last_frame = &frame;
+      last_frame_idx = event_data->frames.size() - 1;
       last_id ++;
       last_timestamp = event_data->start_time;
       event_data->frame_count ++;
@@ -339,14 +364,14 @@ bool EventStream::loadEventData(uint64_t event_id) {
         if (event_data->end_time < last_timestamp) break;
         last_id ++;
 
-        auto frame = event_data->frames.emplace_back(
+        auto &frame = event_data->frames.emplace_back(
                        last_id,
                        last_timestamp,
-                       last_frame->offset + delta,
+                       event_data->frames[last_frame_idx].offset + delta,
                        delta,
                        false
                      );
-        last_frame = &frame;
+        last_frame_idx = event_data->frames.size() - 1;
         Debug(3, "Trailing Frame %d timestamp (%f s), offset (%f s), delta(%f s), in_db(%d)",
               last_id,
               FPSeconds(frame.timestamp.time_since_epoch()).count(),
@@ -382,10 +407,13 @@ bool EventStream::loadEventData(uint64_t event_id) {
 
   // Not sure about this
   if ( forceEventChange || mode == MODE_ALL_GAPLESS ) {
-    if ( replay_rate > 0 )
+    if (event_data->frames.empty()) {
+      curr_stream_time = event_data->start_time;
+    } else if ( replay_rate > 0 ) {
       curr_stream_time = event_data->frames[0].timestamp;
-    else
-      curr_stream_time = event_data->frames[event_data->last_frame_id-1].timestamp;
+    } else {
+      curr_stream_time = event_data->frames[event_data->frames.size()-1].timestamp;
+    }
   }
   Debug(2, "Event: %" PRIu64 ", Frames: %d, Last Frame ID (%d, Duration: %.2f s Frames Duration: %.2f s",
         event_data->event_id,
@@ -414,7 +442,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
     if (
       (mode == MODE_SINGLE || mode == MODE_NONE)
       &&
-      (curr_frame_id == event_data->last_frame_id)
+      (curr_frame_id >= (int)event_data->frames.size())
     ) {
       Debug(1, "Was in single or no replay mode, and at last frame, so jumping to 1st frame");
       curr_frame_id = 1;
@@ -478,7 +506,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
     paused = true;
     replay_rate = ZM_RATE_BASE;
     step = 1;
-    if (curr_frame_id < event_data->last_frame_id)
+    if (curr_frame_id < (int)event_data->frames.size())
       curr_frame_id += 1;
     Debug(1, "Got SLOWFWD command new frame id %d", curr_frame_id);
     break;
@@ -569,13 +597,13 @@ void EventStream::processCommand(const CmdMsg *msg) {
     break;
   case CMD_PREV :
     Debug(1, "Got PREV command");
-    curr_frame_id = replay_rate >= 0 ? 1 : event_data->last_frame_id+1;
+    curr_frame_id = replay_rate >= 0 ? 0 : (int)event_data->frames.size()+1;
     paused = false;
     forceEventChange = true;
     break;
   case CMD_NEXT :
     Debug(1, "Got NEXT command");
-    curr_frame_id = replay_rate >= 0 ? event_data->last_frame_id+1 : 1;
+    curr_frame_id = replay_rate >= 0 ? (int)event_data->frames.size()+1 : 0;
     paused = false;
     forceEventChange = true;
     break;
@@ -595,46 +623,29 @@ void EventStream::processCommand(const CmdMsg *msg) {
     }
 
     std::scoped_lock lck{mutex};
-    // This should get us close, but not all frames will have the same duration
-    curr_frame_id = (int) (event_data->frame_count * offset / event_data->duration) + 1;
-    if (curr_frame_id < 1) {
-      Debug(1, "curr_frame_id = %d, so setting to 1", curr_frame_id);
+
+    if (event_data->frames.empty()) {
+      Debug(1, "No frames in event, can't seek");
       curr_frame_id = 1;
-    } else if (curr_frame_id > event_data->last_frame_id) {
-      curr_frame_id = event_data->last_frame_id;
+      curr_stream_time = event_data->start_time;
+      break;
     }
 
-    // TODO Replace this with a binary search
-    if (event_data->frames[curr_frame_id - 1].offset > offset) {
-      Debug(1, "Searching for frame at %.6f, offset of frame %d is %.6f",
-            FPSeconds(offset).count(),
-            curr_frame_id,
-            FPSeconds(event_data->frames[curr_frame_id - 1].offset).count()
-           );
-      while ((curr_frame_id--) && (event_data->frames[curr_frame_id - 1].offset > offset)) {
-        Debug(1, "Searching for frame at %.6f, offset of frame %d is %.6f",
-              FPSeconds(offset).count(),
-              curr_frame_id,
-              FPSeconds(event_data->frames[curr_frame_id - 1].offset).count()
-             );
-      }
-    } else if (event_data->frames[curr_frame_id - 1].offset < offset) {
-      while ((curr_frame_id++ < event_data->last_frame_id) && (event_data->frames[curr_frame_id - 1].offset < offset)) {
-        Debug(1, "Searching for frame at %.6f, offset of frame %d is %.6f",
-              FPSeconds(offset).count(),
-              curr_frame_id,
-              FPSeconds(event_data->frames[curr_frame_id - 1].offset).count()
-             );
-      }
-      curr_frame_id--;
-    }
+    // Binary search: frames are sorted by offset (ascending).
+    // Find the last frame whose offset <= the target offset.
+    auto it = std::upper_bound(
+      event_data->frames.begin(),
+      event_data->frames.end(),
+      offset,
+      [](const FPSeconds &o, const FrameData &frame) {
+        return o < frame.offset;
+      });
 
-    if (curr_frame_id < 1) {
-      Debug(1, "curr_frame_id = %d, so setting to 1", curr_frame_id);
-      curr_frame_id = 1;
-    } else if (curr_frame_id > event_data->last_frame_id) {
-      curr_frame_id = event_data->last_frame_id;
-    }
+    // upper_bound gives first frame with offset > target; step back one
+    if (it != event_data->frames.begin())
+      --it;
+
+    curr_frame_id = static_cast<int>(std::distance(event_data->frames.begin(), it)) + 1;
 
     curr_stream_time = event_data->frames[curr_frame_id-1].timestamp;
     Debug(1, "Got SEEK command, to %f s (new current frame id: %d offset %f s)",
@@ -680,7 +691,9 @@ void EventStream::processCommand(const CmdMsg *msg) {
     //status_data.duration = event_data->duration;
     status_data.duration = FPSeconds(event_data->duration).count();
     //status_data.progress = event_data->frames[curr_frame_id-1].offset;
-    status_data.progress = std::chrono::duration<double>(event_data->frames[curr_frame_id-1].offset).count();
+    status_data.progress = (!event_data->frames.empty() && curr_frame_id >= 1 && curr_frame_id <= (int)event_data->frames.size())
+      ? std::chrono::duration<double>(event_data->frames[curr_frame_id-1].offset).count()
+      : 0.0;
     status_data.rate = replay_rate;
     status_data.zoom = zoom;
     status_data.scale = scale;
@@ -732,12 +745,13 @@ bool EventStream::checkEventLoaded() {
     sql = stringtf(
             "SELECT `Id` FROM `Events` WHERE `MonitorId` = %d AND `Id` < %" PRIu64 " ORDER BY `Id` DESC LIMIT 1",
             event_data->monitor_id, event_data->event_id);
-  } else if (curr_frame_id > event_data->last_frame_id) {
+  } else if (curr_frame_id > (int)event_data->frames.size()) {
     if (event_data->end_time.time_since_epoch() == Seconds(0)) {
       // We are viewing an in-process event, so just reload it.
       loadEventData(event_data->event_id);
-      if (curr_frame_id > event_data->last_frame_id)
-        curr_frame_id = event_data->last_frame_id;
+      int num_frames = (int)event_data->frames.size();
+      if (num_frames == 0 || curr_frame_id > num_frames)
+        curr_frame_id = num_frames > 0 ? num_frames : 1;
       return false;
     }
     sql = stringtf(
@@ -745,8 +759,8 @@ bool EventStream::checkEventLoaded() {
             event_data->monitor_id, event_data->event_id);
   } else {
     // No event change required
-    Debug(4, "No event change required, as curr frame %d <=> event frames %d",
-          curr_frame_id, event_data->frame_count);
+    Debug(4, "No event change required, as curr frame %d <=> event frames %zu",
+          curr_frame_id, event_data->frames.size());
     return false;
   }
 
@@ -773,14 +787,16 @@ bool EventStream::checkEventLoaded() {
 
       loadEventData(event_id);
 
-      curr_frame_id = replay_rate < 0 ? event_data->last_frame_id : 1;
+      int num_frames = (int)event_data->frames.size();
+      curr_frame_id = replay_rate < 0 ? (num_frames > 0 ? num_frames : 1) : 1;
       Debug(2, "New frame id = %d", curr_frame_id);
       start = std::chrono::steady_clock::now();
       mysql_free_result(result);
       return true;
     } else {
       Debug(2, "No next event loaded using %s. Pausing", sql.c_str());
-      curr_frame_id = curr_frame_id <= 0 ? 1 : event_data->frame_count;
+      int num_frames = (int)event_data->frames.size();
+      curr_frame_id = curr_frame_id <= 0 ? 1 : (num_frames > 0 ? num_frames : 1);
       paused = true;
       sendTextFrame("No more event data found");
     }  // end if found a new event or not
@@ -788,8 +804,13 @@ bool EventStream::checkEventLoaded() {
     forceEventChange = false;
   } else {
     Debug(2, "Pausing because mode is %s", StreamMode_Strings[mode].c_str());
-    curr_frame_id = curr_frame_id <= 0 ? 1 : event_data->last_frame_id;
+    int num_frames = (int)event_data->frames.size();
+    curr_frame_id = curr_frame_id <= 0 ? 1 : (num_frames > 0 ? num_frames : 1);
     paused = true;
+    // Reset last_frame_sent so the next loop iteration sends a keepalive
+    // immediately, rather than leaving a 5-second gap (MAX_STREAM_DELAY)
+    // during which the HTTP connection can time out and break the pipe.
+    last_frame_sent = {};
   }
   return false;
 }  // void EventStream::checkEventLoaded()
@@ -804,6 +825,17 @@ Image * EventStream::getImage( ) {
 bool EventStream::sendFrame(Microseconds delta_us) {
   Debug(2, "Sending frame %d", curr_frame_id);
 
+  if (event_data->frames.empty()) {
+    Debug(1, "No frames in event, can't send frame");
+    return true;  // Not fatal, just nothing to send yet
+  }
+
+  if (curr_frame_id < 1 || curr_frame_id > (int)event_data->frames.size()) {
+    Debug(1, "curr_frame_id %d out of range [1..%zu], clamping",
+          curr_frame_id, event_data->frames.size());
+    curr_frame_id = std::clamp(curr_frame_id, 1, (int)event_data->frames.size());
+  }
+
   std::string filepath;
   struct stat filestat = {};
 
@@ -1046,39 +1078,37 @@ void EventStream::runStream() {
 
         continue;
       }  // end if !in_event
-    }  // end scope for mutex lock
 
-    if (send_frame) {
-      if (!sendFrame(delta)) {
-        zm_terminate = true;
-        break;
-      }
-      if (send_twice and !sendFrame(delta)) {
-        zm_terminate = true;
-        break;
+      if (send_frame) {
+        if (!sendFrame(delta)) {
+          zm_terminate = true;
+          break;
+        }
+        if (send_twice and !sendFrame(delta)) {
+          zm_terminate = true;
+          break;
+        }
+        frame_count++;
       }
-      frame_count++;
-    }
 
-    {
-      std::scoped_lock lck{mutex};
-
-      if (!paused) {
+      if (!paused && !event_data->frames.empty()
+          && curr_frame_id >= 1 && curr_frame_id <= (int)event_data->frames.size()) {
         // Get current frame data, curr_frame_id may have changed
         FrameData *last_frame_data = &event_data->frames[curr_frame_id-1];
         curr_stream_time = last_frame_data->timestamp;
         curr_frame_id += (replay_rate > 0 ? frame_mod : -1*frame_mod);
 
-        // we incremented by replay_rate, so might have jumped past frame_count
+        // we incremented by replay_rate, so might have jumped past frames.size()
+        int num_frames = (int)event_data->frames.size();
         if ((mode == MODE_SINGLE) && (
-              (curr_frame_id < 1 ) || (curr_frame_id >= event_data->frame_count)
+              (curr_frame_id < 1 ) || (curr_frame_id > num_frames)
             )
            ) {
           Debug(2, "Have mode==MODE_SINGLE and at end of event, looping back to start");
           curr_frame_id = 1;
         }
 
-        if (curr_frame_id <= event_data->frame_count) {
+        if (curr_frame_id >= 1 && curr_frame_id <= num_frames) {
           const FrameData *next_frame_data = &event_data->frames[curr_frame_id-1];
           Debug(3, "Have Frame %d %d timestamp (%f s), offset (%f s) delta (%f s), in_db (%d)",
                 curr_frame_id, next_frame_data->id,
@@ -1160,7 +1190,9 @@ void EventStream::runStream() {
 
         // This next bit is to determine if we are in the current event time wise
         // and whether to show an image saying how long until the next event.
-        if (replay_rate > 0) {
+        if (event_data->frames.empty()) {
+          Debug(1, "New event has no frames yet, skipping time_to_event calculation");
+        } else if (replay_rate > 0) {
           // This doesn't make sense unless we have hit the end of the event.
           time_to_event = event_data->frames[0].timestamp - curr_stream_time;
           Debug(1, "replay rate (%d) time_to_event (%f s) = frame timestamp (%f s) - curr_stream_time (%f s)",
@@ -1170,12 +1202,12 @@ void EventStream::runStream() {
                 FPSeconds(curr_stream_time.time_since_epoch()).count());
 
         } else if (replay_rate < 0) {
-          time_to_event = curr_stream_time - event_data->frames[event_data->frame_count-1].timestamp;
+          time_to_event = curr_stream_time - event_data->frames[event_data->frames.size()-1].timestamp;
           Debug(1, "replay rate (%d), time_to_event(%f s) = curr_stream_time (%f s) - frame timestamp (%f s)",
                 replay_rate,
                 FPSeconds(time_to_event).count(),
                 FPSeconds(curr_stream_time.time_since_epoch()).count(),
-                FPSeconds(event_data->frames[event_data->frame_count - 1].timestamp.time_since_epoch()).count());
+                FPSeconds(event_data->frames[event_data->frames.size() - 1].timestamp.time_since_epoch()).count());
         }  // end if forward or reverse
       }  // end if checkEventLoaded
     }  // end scope for lock
diff --git a/web/skins/classic/views/js/event.js b/web/skins/classic/views/js/event.js
index 8d6121e4a..87560edb1 100644
--- a/web/skins/classic/views/js/event.js
+++ b/web/skins/classic/views/js/event.js
@@ -544,14 +544,7 @@ function playClicked( ) {
     }
   } else {
     if (zmsBroke) {
-      // The assumption is that the command failed because zms exited, so restart the stream.
-      const img = document.getElementById('evtStream');
-      const src = img.src;
-      const url = new URL(src);
-      url.searchParams.set('scale', currentScale); // In event.php we don’t yet know what scale to substitute. Let it be for now.
-      img.src = '';
-      img.src = url;
-      zmsBroke = false;
+      restartZmsStream();
     } else {
       streamReq({command: CMD_PLAY});
     }
@@ -819,11 +812,33 @@ function streamPan(x, y) {
 }
 */
 
+// Restart the zms stream by resetting the <img> src. The browser makes a
+// new CGI request to zms (same connkey), spawning a fresh process. When the
+// new zms delivers its first MJPEG frame, img.onload fires as a reliable
+// signal that the command socket is ready. Optional onReady callback is
+// invoked at that point to send queued commands (e.g. CMD_SEEK).
+function restartZmsStream(onReady) {
+  const img = document.getElementById('evtStream');
+  const url = new URL(img.src);
+  url.searchParams.set('scale', currentScale);
+  img.src = '';
+  img.onload = function() {
+    img.onload = null;
+    zmsBroke = false;
+    if (onReady) onReady();
+  };
+  img.src = url.href;
+}
+
 function streamSeek(offset) {
   if (vid) {
     vid.currentTime(offset);
   } else {
-    streamReq({command: CMD_SEEK, offset: offset});
+    if (zmsBroke) {
+      restartZmsStream(function() { streamSeek(offset); });
+    } else {
+      streamReq({command: CMD_SEEK, offset: offset});
+    }
   }
 }
 
