Merge branch 'master' into patch-401263

This commit is contained in:
IgorA100
2026-05-18 00:50:18 +03:00
committed by GitHub
20 changed files with 334 additions and 169 deletions

View File

@@ -202,9 +202,12 @@ Event::~Event() {
/* Close the video file */
// We close the videowriter first, because if we finish the event, we might try to view the file, but we aren't done writing it yet.
if (videoStore != nullptr) {
// Finalize last fragment before closing the video store
// Flush the trailer + record the final fragment before writing the m3u8.
// finalize() must run before writeM3U8 so the manifest contains every
// fragment (including the one no later keyframe was around to close).
videoStore->finalize();
std::string m3u8_path = path + "/index.m3u8";
// Write temporary m3u8 with incomplete filename (writeM3U8 finalizes last fragment)
std::string video_url_tmp = "index.php?view=view_video&eid=" + std::to_string(id)
+ "&file=" + video_incomplete_file;
videoStore->writeM3U8(m3u8_path, video_url_tmp, true);

View File

@@ -449,10 +449,12 @@ void EventStream::processCommand(const CmdMsg *msg) {
switch ((MsgCommand)msg->msg_data[0]) {
case CMD_PAUSE :
Debug(1, "Got PAUSE command");
stopped = false;
paused = true;
break;
case CMD_PLAY : {
Debug(1, "Got PLAY command");
stopped = false;
paused = false;
// If we are in single event mode and at the last frame, replay the current event
@@ -476,8 +478,9 @@ void EventStream::processCommand(const CmdMsg *msg) {
}
case CMD_VARPLAY : {
Debug(1, "Got VARPLAY command");
stopped = false;
paused = false;
replay_rate = ntohs(((unsigned char)msg->msg_data[2]<<8)|(unsigned char)msg->msg_data[1])-32768;
replay_rate = (((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2])-VARPLAY_RATE_OFFSET;
if (replay_rate > 50 * ZM_RATE_BASE) {
Warning("requested replay rate (%d) is too high. We only support up to 50x", replay_rate);
replay_rate = 50 * ZM_RATE_BASE;
@@ -489,10 +492,14 @@ void EventStream::processCommand(const CmdMsg *msg) {
}
case CMD_STOP :
Debug(1, "Got STOP command");
stopped = true;
paused = false;
step = 0;
send_twice = false;
break;
case CMD_FASTFWD : {
Debug(1, "Got FAST FWD command");
stopped = false;
paused = false;
// Set play rate
switch (replay_rate) {
@@ -517,6 +524,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
break;
}
case CMD_SLOWFWD : {
stopped = false;
paused = true;
replay_rate = ZM_RATE_BASE;
step = 1;
@@ -526,6 +534,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
break;
}
case CMD_SLOWREV : {
stopped = false;
paused = true;
replay_rate = ZM_RATE_BASE;
step = -1;
@@ -535,6 +544,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
}
case CMD_FASTREV :
Debug(1, "Got FAST REV command");
stopped = false;
paused = false;
// Set play rate
switch (replay_rate) {
@@ -693,6 +703,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
int zoom;
int scale;
bool paused;
bool stopped;
} status_data = {};
{
@@ -707,6 +718,7 @@ void EventStream::processCommand(const CmdMsg *msg) {
status_data.zoom = zoom;
status_data.scale = scale;
status_data.paused = paused;
status_data.stopped = stopped;
FPSeconds elapsed = now - last_fps_update;
if (elapsed.count() > 0) {
@@ -719,10 +731,11 @@ void EventStream::processCommand(const CmdMsg *msg) {
status_data.fps = actual_fps;
Debug(2, "Event:%" PRIu64 ", Duration %f, Paused:%d, progress:%f Rate:%d, Zoom:%d Scale:%d",
Debug(2, "Event:%" PRIu64 ", Duration %f, Paused:%d, Stopped:%d, progress:%f Rate:%d, Zoom:%d Scale:%d",
status_data.event_id,
FPSeconds(status_data.duration).count(),
status_data.paused,
status_data.stopped,
FPSeconds(status_data.progress).count(),
status_data.rate,
status_data.zoom,
@@ -1037,7 +1050,11 @@ void EventStream::runStream() {
send_frame = false;
TimePoint::duration time_since_last_send = now - last_frame_sent;
if (!paused) {
if (stopped) {
// In stopped state, skip all frame processing until a new command is received.
// send_frame is already false from initialization above.
delta = MAX_SLEEP;
} else if (!paused) {
// Figure out if we should send this frame
Debug(3, "not paused at curr_frame_id (%d-1) mod frame_mod(%d)", curr_frame_id, frame_mod);
// If we are streaming and this frame is due to be sent
@@ -1069,7 +1086,7 @@ void EventStream::runStream() {
} // end if streaming stepping or doing nothing
// time_to_event > 0 means that we are not in the event
if (time_to_event > Seconds(0) and mode == MODE_ALL) {
if (!stopped && time_to_event > Seconds(0) and mode == MODE_ALL) {
Debug(1, "Time since last send = %.2f s", FPSeconds(time_since_last_send).count());
if (time_since_last_send > Seconds(1)) {
char frame_text[64];
@@ -1117,7 +1134,7 @@ void EventStream::runStream() {
frame_count++;
}
if (!paused && !event_data->frames.empty()
if (!paused && !stopped && !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];
@@ -1180,14 +1197,14 @@ void EventStream::runStream() {
);
} // end if not at end of event
} else {
// Paused
// Paused or stopped
delta = MAX_SLEEP;
// We are paused, so might be stepping
// We are paused, so might be stepping (not when fully stopped)
//if ( step != 0 )// Adding 0 is cheaper than an if 0
// curr_frame_id starts at 1 though, so we might skip the first frame?
curr_frame_id += step;
} // end if !paused
if (!stopped) curr_frame_id += step;
} // end if !paused && !stopped
} // end scope for mutex lock
if (type == STREAM_SINGLE) {

View File

@@ -92,12 +92,14 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
switch ((MsgCommand)msg->msg_data[0]) {
case CMD_PAUSE :
Debug(1, "Got PAUSE command");
stopped = false;
paused = true;
delayed = true;
last_frame_sent = now;
break;
case CMD_PLAY :
Debug(1, "Got PLAY command");
stopped = false;
if (paused) {
paused = false;
delayed = true;
@@ -106,19 +108,24 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
break;
case CMD_VARPLAY :
Debug(1, "Got VARPLAY command");
stopped = false;
if (paused) {
paused = false;
delayed = true;
}
replay_rate = ntohs(((unsigned char)msg->msg_data[2]<<8)|(unsigned char)msg->msg_data[1])-32768;
replay_rate = (((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2])-VARPLAY_RATE_OFFSET;
break;
case CMD_STOP :
Debug(1, "Got STOP command");
paused = true;
stopped = true;
paused = false;
delayed = false;
step = 0;
send_twice = false;
break;
case CMD_FASTFWD :
Debug(1, "Got FAST FWD command");
stopped = false;
if (paused) {
paused = false;
delayed = true;
@@ -156,6 +163,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
}
case CMD_SLOWFWD :
Debug(1, "Got SLOW FWD command");
stopped = false;
paused = true;
delayed = true;
replay_rate = ZM_RATE_BASE;
@@ -163,6 +171,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
break;
case CMD_SLOWREV :
Debug(1, "Got SLOW REV command");
stopped = false;
paused = true;
delayed = true;
replay_rate = ZM_RATE_BASE;
@@ -170,6 +179,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
break;
case CMD_FASTREV :
Debug(1, "Got FAST REV command");
stopped = false;
if (paused) {
paused = false;
delayed = true;
@@ -256,6 +266,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
int score;
int analysing;
bool analysis_image;
bool stopped;
} status_data;
status_data.id = monitor->Id();
@@ -299,6 +310,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
} // end monitor_mutex scope
status_data.delayed = delayed;
status_data.paused = paused;
status_data.stopped = stopped;
status_data.rate = replay_rate;
status_data.delay = FPSeconds(now - last_frame_sent).count();
status_data.zoom = zoom;
@@ -306,13 +318,14 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
status_data.analysis_image = (frame_type == FRAME_ANALYSIS) &&
monitor->ShmValid() &&
(monitor->Analysing() != Monitor::ANALYSING_NONE);
Debug(2, "viewing fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d score: %d analysis_image: %d",
Debug(2, "viewing fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Stopped:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d score: %d analysis_image: %d",
status_data.fps,
status_data.capture_fps,
status_data.analysis_fps,
status_data.buffer_level,
status_data.delayed,
status_data.paused,
status_data.stopped,
status_data.rate,
status_data.delay,
status_data.zoom,
@@ -635,6 +648,12 @@ void MonitorStream::runStream() {
std::this_thread::sleep_for(MAX_SLEEP);
continue;
}
if (stopped) {
// In stopped state, do nothing except wait for a new command.
// Don't call setLastViewed() so we don't keep capture/decoding active unnecessarily.
std::this_thread::sleep_for(MAX_SLEEP);
continue;
}
monitor->setLastViewed();
if (frame_type == FRAME_ANALYSIS)
monitor->setLastAnalysisViewed();

View File

@@ -137,9 +137,17 @@ RETSIGTYPE zm_die_handler(int signal)
ip = (void *)(uc->uc_mcontext.gregs[REG_EIP]);
#endif
#elif defined(__aarch64__)
#if defined(__FreeBSD__)
ip = (void *)(uc->uc_mcontext.mc_gpregs.gp_elr);
#else
ip = (void *)(uc->uc_mcontext.pc);
#endif
#elif defined(__arm__)
#if defined(__FreeBSD__)
ip = (void *)(uc->uc_mcontext.__gregs[_REG_PC]);
#else
ip = (void *)(uc->uc_mcontext.arm_pc);
#endif
#endif
// Print the fault address and instruction pointer

View File

@@ -54,6 +54,9 @@ class StreamBase {
enum { DEFAULT_ZOOM=ZM_SCALE_BASE };
enum { DEFAULT_MAXFPS=10 };
enum { DEFAULT_BITRATE=100000 };
// Offset applied when encoding a signed replay rate as a uint16 for CMD_VARPLAY.
// On the wire: uint16 = rate + VARPLAY_RATE_OFFSET. Receiver subtracts the same offset.
static const int VARPLAY_RATE_OFFSET = 32768;
protected:
typedef struct {
@@ -75,7 +78,11 @@ class StreamBase {
typedef enum {
CMD_NONE=0,
CMD_PAUSE,
// CMD_PLAY resumes or starts playback at normal speed (1x, i.e. replay_rate = ZM_RATE_BASE).
// Use CMD_VARPLAY to resume at an arbitrary rate.
CMD_PLAY,
// CMD_STOP halts all streaming activity. Unlike CMD_PAUSE, no keepalive frames are sent
// and the stream does no work until a new command is received.
CMD_STOP,
CMD_FASTFWD,
CMD_SLOWFWD,
@@ -88,6 +95,13 @@ class StreamBase {
CMD_PREV,
CMD_NEXT,
CMD_SEEK,
// CMD_VARPLAY resumes or starts playback at a caller-specified rate.
// The desired rate is packed as a big-endian uint16 offset by +VARPLAY_RATE_OFFSET so that
// the range [-32768, +32767] maps to [0, 65535]. ZM_RATE_BASE (100) represents 1x speed, so:
// 32868 (= VARPLAY_RATE_OFFSET + 100) encodes 1x forward playback,
// 32668 (= VARPLAY_RATE_OFFSET - 100) encodes 1x reverse playback.
// Negative rates play in reverse; rates > ZM_RATE_BASE play faster than real-time.
// MSG payload: msg_data[1..2] = (rate + VARPLAY_RATE_OFFSET) as network-byte-order uint16.
CMD_VARPLAY,
CMD_GET_IMAGE,
CMD_QUIT,
@@ -125,6 +139,7 @@ class StreamBase {
char sock_path_lock[108];
int lock_fd;
bool paused;
bool stopped;
int step;
bool send_twice; // flag to send the same frame twice
@@ -188,7 +203,9 @@ class StreamBase {
sd(-1),
lock_fd(0),
paused(false),
stopped(false),
step(0),
send_twice(false),
maxfps(DEFAULT_MAXFPS),
base_fps(0.0),
effective_fps(0.0),

View File

@@ -62,7 +62,7 @@ VideoStore::VideoStore(
resample_ctx(nullptr),
fifo(nullptr),
converted_in_samples(nullptr),
filename(filename_in),
filename(filename_in ? filename_in : ""),
format(format_in),
video_first_pts(AV_NOPTS_VALUE),
video_first_dts(AV_NOPTS_VALUE),
@@ -76,7 +76,8 @@ VideoStore::VideoStore(
reorder_queue_size(0),
last_fragment_offset_(0),
last_fragment_start_dts_(AV_NOPTS_VALUE),
init_segment_end_(0) {
init_segment_end_(0),
finalized_(false) {
FFMPEGInit();
swscale.init();
opkt = av_packet_ptr{av_packet_alloc()};
@@ -84,24 +85,24 @@ VideoStore::VideoStore(
/* Failure to open audio will not be a total failure. */
bool VideoStore::open() {
Debug(1, "Opening video storage stream %s format: %s", filename, format);
Debug(1, "Opening video storage stream %s format: %s", filename.c_str(), format);
int ret = avformat_alloc_output_context2(&oc, nullptr, nullptr, filename);
int ret = avformat_alloc_output_context2(&oc, nullptr, nullptr, filename.c_str());
if (ret < 0) {
Warning(
"Could not create video storage stream %s as no out ctx"
" could be assigned based on filename: %s",
filename, av_make_error_string(ret).c_str());
filename.c_str(), av_make_error_string(ret).c_str());
}
// Couldn't deduce format from filename, trying from format name
if (!oc) {
avformat_alloc_output_context2(&oc, nullptr, format, filename);
avformat_alloc_output_context2(&oc, nullptr, format, filename.c_str());
if (!oc) {
Error(
"Could not create video storage stream %s as no out ctx"
" could not be assigned based on filename or format %s",
filename, format);
filename.c_str(), format);
return false;
}
} // end if ! oc
@@ -521,9 +522,9 @@ bool VideoStore::open() {
/* open the out file, if needed */
if (!(out_format->flags & AVFMT_NOFILE)) {
ret = avio_open2(&oc->pb, filename, AVIO_FLAG_WRITE, nullptr, nullptr);
ret = avio_open2(&oc->pb, filename.c_str(), AVIO_FLAG_WRITE, nullptr, nullptr);
if (ret < 0) {
Error("Could not open out file '%s': %s", filename, av_make_error_string(ret).c_str());
Error("Could not open out file '%s': %s", filename.c_str(), av_make_error_string(ret).c_str());
return false;
}
}
@@ -562,7 +563,7 @@ bool VideoStore::open() {
av_dict_free(&opts);
if (ret < 0) {
Error("Error occurred when writing out file header to %s: %s",
filename, av_make_error_string(ret).c_str());
filename.c_str(), av_make_error_string(ret).c_str());
avio_closep(&oc->pb);
return false;
}
@@ -691,49 +692,11 @@ Debug(1, "Done flushing");
VideoStore::~VideoStore() {
for (auto &n : reorder_queues) {
auto &queue = n.second;
Debug(1, "Queue for %d length is %zu", n.first, queue.size());
while (!queue.empty()) {
auto pkt = queue.front();
queue.pop_front();
if (pkt->codec_type == AVMEDIA_TYPE_VIDEO) {
writeVideoFramePacket(pkt);
} else if (pkt->codec_type == AVMEDIA_TYPE_AUDIO) {
writeAudioFramePacket(pkt);
}
//delete pkt;
}
}
if (oc->pb) {
flush_codecs();
// Flush Queues
Debug(4, "Flushing interleaved queues");
av_interleaved_write_frame(oc, nullptr);
Debug(1, "Writing trailer");
/* Write the trailer before close */
int rc;
if ((rc = av_write_trailer(oc)) < 0) {
Error("Error writing trailer %s", av_err2str(rc));
} else {
Debug(3, "Success Writing trailer");
}
// When will we not be using a file ?
if (!(out_format->flags & AVFMT_NOFILE)) {
/* Close the out file. */
Debug(4, "Closing");
if ((rc = avio_close(oc->pb)) < 0) {
Error("Error closing avio %s", av_err2str(rc));
}
} else {
Debug(3, "Not closing avio because we are not writing to a file.");
}
oc->pb = nullptr;
} // end if oc->pb
// Run the shutdown path through finalize() so the queue-drain / trailer /
// close logic lives in one place. finalize() is idempotent and bails early
// if oc was never allocated, so the legacy "caller didn't call finalize"
// path and the open()-failed-before-allocating-oc path both work.
finalize();
// I wonder if we should be closing the file first.
// I also wonder if we really need to be doing all the ctx
@@ -1551,26 +1514,31 @@ int VideoStore::write_packet(AVPacket *pkt, AVStream *stream) {
Debug(3, "next_dts for stream %d has become %" PRId64 " last_dts %" PRId64,
stream->index, next_dts[stream->index], last_dts[stream->index]);
// HLS fragment tracking: with frag_keyframe movflag, FFmpeg creates a new
// moof+mdat at each video keyframe. We record the byte range of each fragment
// by checking the file position before and after the write call.
//
// Strategy: before writing a video keyframe, snapshot the file position.
// This marks the end of the previous fragment. We record that fragment and
// start tracking the new one.
bool is_video_keyframe = (stream == video_out_stream) && (pkt->flags & AV_PKT_FLAG_KEY);
// Snapshot the keyframe's dts before the write call may modify the packet.
int64_t this_keyframe_dts = is_video_keyframe ? pkt->dts : AV_NOPTS_VALUE;
int ret = av_interleaved_write_frame(oc, pkt);
if (ret != 0) {
Error("Error writing packet: %s", av_make_error_string(ret).c_str());
} else {
Debug(4, "Success writing packet");
}
// HLS fragment tracking: with movflags=frag_keyframe, the muxer flushes the
// previous fragment to disk inside av_interleaved_write_frame() when a new
// keyframe arrives. So the position *after* this call equals the end of the
// just-flushed fragment, and last_fragment_offset_/_dts_ describe that
// fragment. Record it, then move tracking to the new fragment.
if (is_video_keyframe && oc && oc->pb) {
// Force flush any buffered data so the file position reflects all previous writes
avio_flush(oc->pb);
int64_t pos_now = avio_tell(oc->pb);
int64_t pos_after = avio_tell(oc->pb);
if (last_fragment_start_dts_ != AV_NOPTS_VALUE && pos_now > last_fragment_offset_) {
// Record the completed fragment
int64_t frag_size = pos_now - last_fragment_offset_;
if (last_fragment_start_dts_ != AV_NOPTS_VALUE && pos_after > last_fragment_offset_) {
int64_t frag_size = pos_after - last_fragment_offset_;
double duration = 0;
if (video_out_stream->time_base.den > 0) {
duration = static_cast<double>(pkt->dts - last_fragment_start_dts_)
duration = static_cast<double>(this_keyframe_dts - last_fragment_start_dts_)
* video_out_stream->time_base.num
/ video_out_stream->time_base.den;
}
@@ -1580,49 +1548,118 @@ int VideoStore::write_packet(AVPacket *pkt, AVStream *stream) {
fragments_.size() - 1, last_fragment_offset_, frag_size, duration);
}
}
// New fragment starts here
last_fragment_offset_ = pos_now;
last_fragment_start_dts_ = pkt->dts;
}
// Initialize tracking after init segment is written
if (last_fragment_start_dts_ == AV_NOPTS_VALUE && is_video_keyframe) {
if (oc && oc->pb) {
last_fragment_offset_ = avio_tell(oc->pb);
}
last_fragment_start_dts_ = pkt->dts;
}
int ret = av_interleaved_write_frame(oc, pkt);
if (ret != 0) {
Error("Error writing packet: %s", av_make_error_string(ret).c_str());
} else {
Debug(4, "Success writing packet");
last_fragment_offset_ = pos_after;
last_fragment_start_dts_ = this_keyframe_dts;
}
return ret;
} // end int VideoStore::write_packet(AVPacket *pkt, AVStream *stream)
void VideoStore::writeM3U8(const std::string &m3u8_path, const std::string &video_url, bool is_complete) {
// Finalize last fragment if there's data after the last recorded fragment
if (oc && oc->pb) {
int64_t file_end = avio_tell(oc->pb);
if (file_end > last_fragment_offset_ && last_fragment_start_dts_ != AV_NOPTS_VALUE) {
int64_t frag_size = file_end - last_fragment_offset_;
// Estimate duration from last known DTS
double duration = 0;
if (video_out_stream && video_out_stream->time_base.den > 0 &&
last_dts.count(video_out_stream->index) && last_dts[video_out_stream->index] != AV_NOPTS_VALUE) {
duration = static_cast<double>(last_dts[video_out_stream->index] + last_duration[video_out_stream->index] - last_fragment_start_dts_)
* video_out_stream->time_base.num
/ video_out_stream->time_base.den;
}
if (duration > 0 && frag_size > 0) {
fragments_.push_back({last_fragment_offset_, frag_size, duration});
void VideoStore::finalize() {
if (finalized_) return;
finalized_ = true;
if (!oc || !oc->pb) return;
// Drain reorder queues before writing the trailer — the destructor would
// otherwise try to run these packets through av_interleaved_write_frame()
// after we've already closed oc->pb here.
for (auto &n : reorder_queues) {
auto &queue = n.second;
Debug(1, "Queue for %d length is %zu", n.first, queue.size());
while (!queue.empty()) {
auto pkt = queue.front();
queue.pop_front();
if (pkt->codec_type == AVMEDIA_TYPE_VIDEO) {
writeVideoFramePacket(pkt);
} else if (pkt->codec_type == AVMEDIA_TYPE_AUDIO) {
writeAudioFramePacket(pkt);
}
}
}
flush_codecs();
Debug(4, "Flushing interleaved queues");
av_interleaved_write_frame(oc, nullptr);
Debug(1, "Writing trailer");
int rc = av_write_trailer(oc);
if (rc < 0) {
Error("Error writing trailer %s", av_err2str(rc));
} else {
Debug(3, "Success Writing trailer");
}
// After av_write_trailer, the file contains init+fragments_1..N + mfra trailer.
// Capture the on-disk length so we can size the final fragment.
avio_flush(oc->pb);
int64_t file_size = avio_tell(oc->pb);
// Close the output file before reading it back to inspect the mfra box.
if (!(out_format->flags & AVFMT_NOFILE)) {
Debug(4, "Closing");
if ((rc = avio_close(oc->pb)) < 0) {
Error("Error closing avio %s", av_err2str(rc));
}
}
oc->pb = nullptr;
// The MOV muxer writes an mfra (Movie Fragment Random Access) box at the end
// of the file when fragmentation is on. Its trailing mfro box is exactly 16
// bytes and contains the mfra size, so we can subtract that to find where
// the final fragment's mdat actually ends.
int64_t fragment_n_end = file_size;
if (!filename.empty() && file_size >= 16) {
FILE *fp = fopen(filename.c_str(), "rb");
if (fp) {
if (fseeko(fp, file_size - 16, SEEK_SET) == 0) {
uint8_t mfro[16];
if (fread(mfro, 1, 16, fp) == 16) {
uint32_t box_size = (static_cast<uint32_t>(mfro[0]) << 24)
| (static_cast<uint32_t>(mfro[1]) << 16)
| (static_cast<uint32_t>(mfro[2]) << 8)
| static_cast<uint32_t>(mfro[3]);
if (box_size == 16
&& mfro[4] == 'm' && mfro[5] == 'f' && mfro[6] == 'r' && mfro[7] == 'o') {
uint32_t mfra_size = (static_cast<uint32_t>(mfro[12]) << 24)
| (static_cast<uint32_t>(mfro[13]) << 16)
| (static_cast<uint32_t>(mfro[14]) << 8)
| static_cast<uint32_t>(mfro[15]);
if (mfra_size > 0 && static_cast<int64_t>(mfra_size) <= file_size) {
fragment_n_end = file_size - mfra_size;
Debug(1, "mfra trailer is %u bytes; final fragment ends at %" PRId64,
mfra_size, fragment_n_end);
}
}
}
}
fclose(fp);
}
}
// Record the final fragment that no subsequent keyframe was around to record.
if (last_fragment_start_dts_ != AV_NOPTS_VALUE
&& fragment_n_end > last_fragment_offset_
&& video_out_stream && video_out_stream->time_base.den > 0
&& last_dts.count(video_out_stream->index)
&& last_dts[video_out_stream->index] != AV_NOPTS_VALUE) {
int64_t frag_size = fragment_n_end - last_fragment_offset_;
double duration = static_cast<double>(
last_dts[video_out_stream->index]
+ last_duration[video_out_stream->index]
- last_fragment_start_dts_)
* video_out_stream->time_base.num
/ video_out_stream->time_base.den;
if (duration > 0 && frag_size > 0) {
fragments_.push_back({last_fragment_offset_, frag_size, duration});
Debug(1, "HLS final fragment: offset=%" PRId64 " size=%" PRId64 " duration=%.3f",
last_fragment_offset_, frag_size, duration);
}
}
}
void VideoStore::writeM3U8(const std::string &m3u8_path, const std::string &video_url, bool is_complete) {
if (fragments_.empty()) return;
// Calculate max duration for EXT-X-TARGETDURATION (must be integer, rounded up)

View File

@@ -71,7 +71,10 @@ class VideoStore {
AVAudioFifo *fifo;
uint8_t *converted_in_samples;
const char *filename;
// filename is owned (std::string) so it stays valid for the lifetime of
// VideoStore even if the caller later renames/reassigns the source path
// it was constructed from. A bare const char* would dangle in that case.
std::string filename;
const char *format;
// These are for in
@@ -93,11 +96,17 @@ class VideoStore {
size_t reorder_queue_size;
std::map<int, std::list<std::shared_ptr<ZMPacket>>> reorder_queues;
// HLS fragment tracking
// HLS fragment tracking. With movflags=frag_keyframe, FFmpeg's mov muxer
// doesn't write a fragment to disk until the *next* keyframe arrives (or
// until av_write_trailer is called). So when keyframe N arrives, fragment
// N-1 is what just got flushed. We snapshot avio_tell *after*
// av_interleaved_write_frame() to capture the position past that flush, and
// record fragment N-1 then.
std::vector<Fragment> fragments_;
int64_t last_fragment_offset_; // byte offset where current fragment started
int64_t last_fragment_start_dts_; // DTS of first video keyframe in current fragment
int64_t last_fragment_offset_; // byte offset where the current (in-progress) fragment starts
int64_t last_fragment_start_dts_; // DTS of the keyframe that started the current fragment
int64_t init_segment_end_; // byte offset where init segment (ftyp+moov) ends
bool finalized_; // true once finalize() has run trailer + last-fragment recording
bool setup_resampler();
int write_packet(AVPacket *pkt, AVStream *stream);
@@ -124,6 +133,11 @@ class VideoStore {
const std::vector<Fragment> &fragments() const { return fragments_; }
int64_t init_segment_end() const { return init_segment_end_; }
void writeM3U8(const std::string &path, const std::string &video_url, bool is_complete);
// Flush queues, write trailer, close output, and record the final fragment.
// Call this before writeM3U8(true) so the manifest contains every fragment.
// Safe to call once; subsequent calls are no-ops. The destructor will skip
// the trailer write if finalize() has already run.
void finalize();
const char *get_codec() {
if (chosen_codec_data)

View File

@@ -58,7 +58,9 @@ ajaxError('Unrecognised action '.$_REQUEST['action'].' or insufficient permissio
function queryRequest() {
global $user, $Servers;
require_once('includes/Monitor.php');
require_once('includes/Group.php');
require_once('includes/Group_Monitor.php');
require_once getSkinFile('views/_monitor_filters.php');
$data = array(
'total' => 0,
@@ -92,34 +94,46 @@ function queryRequest() {
$sort = isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'Sequence';
$order = isset($_REQUEST['order']) ? strtoupper($_REQUEST['order']) : 'ASC';
// Build monitor query with filters from request parameters (stateless)
// Build monitor query with filters from request parameters, falling back to cookies
$conditions = array();
$values = array();
// Get filter values directly from request
// Get filter values from request, falling back to cookies for persistence after page refresh.
// getFilterSelection() reads $_REQUEST first, then the zmFilter_* cookie.
$request_filters = array(
'GroupId' => isset($_REQUEST['GroupId']) ? $_REQUEST['GroupId'] : null,
'ServerId' => isset($_REQUEST['ServerId']) ? $_REQUEST['ServerId'] : null,
'StorageId' => isset($_REQUEST['StorageId']) ? $_REQUEST['StorageId'] : null,
'Capturing' => isset($_REQUEST['Capturing']) ? $_REQUEST['Capturing'] : null,
'Analysing' => isset($_REQUEST['Analysing']) ? $_REQUEST['Analysing'] : null,
'Recording' => isset($_REQUEST['Recording']) ? $_REQUEST['Recording'] : null,
'Status' => isset($_REQUEST['Status']) ? $_REQUEST['Status'] : null,
'MonitorId' => isset($_REQUEST['MonitorId']) ? $_REQUEST['MonitorId'] : null,
'MonitorName' => isset($_REQUEST['MonitorName']) ? $_REQUEST['MonitorName'] : null,
'Source' => isset($_REQUEST['Source']) ? $_REQUEST['Source'] : null
'GroupId' => getFilterSelection('GroupId'),
'ServerId' => getFilterSelection('ServerId'),
'StorageId' => getFilterSelection('StorageId'),
'Capturing' => getFilterSelection('Capturing'),
'Analysing' => getFilterSelection('Analysing'),
'Recording' => getFilterSelection('Recording'),
'Status' => getFilterSelection('Status'),
'MonitorId' => getFilterSelection('MonitorId'),
'MonitorName' => getFilterSelection('MonitorName'),
'Source' => getFilterSelection('Source')
);
// Text filters must be strings; guard against a cookie value that happens to be valid JSON.
if (is_array($request_filters['MonitorName'])) $request_filters['MonitorName'] = '';
if (is_array($request_filters['Source'])) $request_filters['Source'] = '';
// Apply request filters to SQL
// Apply GroupId filter using get_group_sql() to include child groups.
// Use validCardinal() to sanitize ID values before use.
if ($request_filters['GroupId']) {
$GroupIds = is_array($request_filters['GroupId']) ? $request_filters['GroupId'] : array($request_filters['GroupId']);
$conditions[] = 'M.Id IN (SELECT MonitorId FROM Groups_Monitors WHERE GroupId IN (' . implode(',', array_fill(0, count($GroupIds), '?')) . '))';
$values = array_merge($values, $GroupIds);
$groupIds = is_array($request_filters['GroupId']) ? $request_filters['GroupId'] : array($request_filters['GroupId']);
$groupIds = array_values(array_filter(array_map('validCardinal', $groupIds)));
if (count($groupIds)) {
$groupSql = ZM\Group::get_group_sql($groupIds);
if ($groupSql) {
$conditions[] = $groupSql;
}
}
}
foreach (array('ServerId','StorageId') as $filter) {
if ($request_filters[$filter]) {
$filter_values = is_array($request_filters[$filter]) ? $request_filters[$filter] : array($request_filters[$filter]);
// Use validCardinal() to sanitize ID values
$filter_values = array_values(array_filter(array_map('validCardinal', $filter_values)));
if (count($filter_values)) {
$conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')';
$values = array_merge($values, $filter_values);
@@ -203,12 +217,15 @@ function queryRequest() {
});
}
// Apply MonitorId filter
// Apply MonitorId filter (use validCardinal() to sanitize ID values)
if ($request_filters['MonitorId']) {
$monitor_ids = is_array($request_filters['MonitorId']) ? $request_filters['MonitorId'] : array($request_filters['MonitorId']);
$filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($monitor_ids) {
return in_array($monitor['Id'], $monitor_ids);
});
$monitor_ids = array_values(array_filter(array_map('validCardinal', $monitor_ids)));
if (count($monitor_ids)) {
$filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($monitor_ids) {
return in_array($monitor['Id'], $monitor_ids);
});
}
}
$data['total'] = count($filtered_monitors);

View File

@@ -151,7 +151,7 @@ default :
$data = unpack('ltype', $msg);
switch ( $data['type'] ) {
case MSG_DATA_WATCH :
$data = unpack('ltype/imonitor/istate/dfps/dcapturefps/danalysisfps/ilevel/irate/ddelay/izoom/iscale/Cdelayed/Cpaused/Cenabled/Cforced/iscore/ianalysing/Canalysisimage', $msg);
$data = unpack('ltype/imonitor/istate/dfps/dcapturefps/danalysisfps/ilevel/irate/ddelay/izoom/iscale/Cdelayed/Cpaused/Cenabled/Cforced/iscore/ianalysing/Canalysisimage/Cstopped', $msg);
$data['fps'] = round( $data['fps'], 2 );
$data['capturefps'] = round( $data['capturefps'], 2 );
$data['analysisfps'] = round( $data['analysisfps'], 2 );
@@ -176,10 +176,10 @@ case MSG_DATA_WATCH :
case MSG_DATA_EVENT :
if ( PHP_INT_SIZE===4 || version_compare( phpversion(), '5.6.0', '<') ) {
ZM\Debug('Using old unpack methods to handle 64bit event id');
$data = unpack('ltype/ieventlow/ieventhigh/dduration/dprogress/dfps/irate/izoom/iscale/Cpaused', $msg);
$data = unpack('ltype/ieventlow/ieventhigh/dduration/dprogress/dfps/irate/izoom/iscale/Cpaused/Cstopped', $msg);
$data['event'] = $data['eventhigh'] << 32 | $data['eventlow'];
} else {
$data = unpack('ltype/Qevent/dduration/dprogress/dfps/irate/izoom/iscale/Cpaused', $msg);
$data = unpack('ltype/Qevent/dduration/dprogress/dfps/irate/izoom/iscale/Cpaused/Cstopped', $msg);
}
$data['rate'] /= RATE_BASE;
$data['zoom'] = round($data['zoom']/SCALE_BASE, 1);

View File

@@ -262,9 +262,14 @@ class EventsController extends AppController {
return;
}
# Get the previous and next events for any monitor
# Get the previous and next events for any monitor.
# Only Id is used below, so skip the wide SELECT + Monitor/Storage joins + Frames hasMany expansion
# that recursive=1 from above would otherwise pull in for each neighbor row.
$this->Event->id = $id;
$event_neighbors = $this->Event->find('neighbors');
$event_neighbors = $this->Event->find('neighbors', array(
'fields' => array('Event.Id'),
'recursive' => -1,
));
$event['Event']['Next'] = isset($event_neighbors['next']) ? $event_neighbors['next']['Event']['Id'] : 0;
$event['Event']['Prev'] = isset($event_neighbors['prev']) ? $event_neighbors['prev']['Event']['Id'] : 0;
@@ -274,7 +279,9 @@ class EventsController extends AppController {
# Also get the previous and next events for the same monitor
$event_monitor_neighbors = $this->Event->find('neighbors', array(
'conditions'=>array('Event.MonitorId'=>$event['Event']['MonitorId'])
'fields' => array('Event.Id'),
'recursive' => -1,
'conditions' => array('Event.MonitorId' => $event['Event']['MonitorId']),
));
$event['Event']['NextOfMonitor'] = isset($event_monitor_neighbors['next']) ? $event_monitor_neighbors['next']['Event']['Id'] : 0;
$event['Event']['PrevOfMonitor'] = isset($event_monitor_neighbors['prev']) ? $event_monitor_neighbors['prev']['Event']['Id'] : 0;

View File

@@ -193,15 +193,6 @@ function getAuthUser($auth) {
$sessionUser = isset($_SESSION['username']) ? $_SESSION['username'] : null;
$filterUser = $requestedUser !== null ? $requestedUser : $sessionUser;
if ($requestedUser !== null && $sessionUser !== null) {
$usersMatch = ZM_CASE_INSENSITIVE_USERNAMES
? (strcasecmp($requestedUser, $sessionUser) === 0)
: ($requestedUser === $sessionUser);
if (!$usersMatch) {
ZM\Warning("Auth user mismatch: URL user='$requestedUser' but session username='$sessionUser'. This may indicate a stale auth hash from a previous login, cross-tab session contamination, or a tampered request.");
}
}
ZM\Debug("getAuthUser: validating auth='$auth' filterUser='".($filterUser ?? '')."' xff='$xff' directAddr='$directAddr' usingRemoteAddr='$remoteAddr' session_username='".($sessionUser ?? '')."'");
$sql = 'SELECT * FROM Users WHERE Enabled = 1';
@@ -257,7 +248,7 @@ function getAuthUser($auth) {
} // end foreach user
} // end if
ZM\Info("Unable to authenticate user from auth hash '$auth' (filterUser='".($filterUser ?? '')."' xff='$xff' directAddr='$directAddr' rowsTried=$rowsTried ttl=".ZM_AUTH_HASH_TTL.'h)');
ZM\Info("Unable to authenticate user from auth hash '$auth' (filterUser='".($filterUser ?? '')."' sessionUser='".($sessionUser ?? '')."' xff='$xff' directAddr='$directAddr' rowsTried=$rowsTried ttl=".ZM_AUTH_HASH_TTL.'h)');
return null;
} // end if using auth hash

View File

@@ -32,6 +32,7 @@ function EventStream(config) {
this.img = null;
this.started = false;
this.paused = false;
this.stopped = false;
this.currentEventId = null;
this.rate = 100;
this.status = null;
@@ -90,6 +91,7 @@ function EventStream(config) {
this.currentEventId = eventId;
this.rate = (options.rate !== undefined) ? options.rate : 100;
this.paused = false;
this.stopped = false;
this.lastOptions = Object.assign({}, options);
// Fresh connkey for this stream
@@ -202,6 +204,7 @@ function EventStream(config) {
this.started = false;
this.paused = false;
this.stopped = false;
this.connKey = null;
this.streamCmdParms.connkey = null;
this.consecutiveErrors = 0;
@@ -247,6 +250,7 @@ function EventStream(config) {
}
this.started = false;
this.connKey = null;
this.stopped = false;
this.streamCmdParms.connkey = null;
// Delay before restarting — exponential backoff
@@ -457,10 +461,13 @@ function EventStream(config) {
}
}
// Track paused state from server
// Track paused and stopped state from server
if (this.status.paused !== undefined) {
this.paused = !!this.status.paused;
}
if (this.status.stopped !== undefined) {
this.stopped = !!this.status.stopped;
}
// Notify consumer
if (this.onStatus) {

View File

@@ -1271,7 +1271,12 @@ function MonitorStream(monitorData) {
const delayString = secsToTime(this.status.delay);
if (this.status.paused == true) {
if (this.status.stopped == true) {
$j('#modeValue'+this.id).text('Stopped');
$j('#rate'+this.id).addClass('hidden');
$j('#delay'+this.id).addClass('hidden');
$j('#level'+this.id).addClass('hidden');
} else if (this.status.paused == true) {
$j('#modeValue'+this.id).text('Paused');
$j('#rate'+this.id).addClass('hidden');
$j('#delayValue'+this.id).text(delayString);

View File

@@ -312,6 +312,10 @@ body #sidebarMain .sub-menu-list {
height: 27px !important;
}
.extruder .extruder-content .chosen-container .chosen-drop {
z-index: 1100;
}
/* Clear Filter Button Select Multiple Selection */
.extruder .extruder-content .term-value-wrapper {
position: relative; /* Enable absolute positioning for child */

View File

@@ -120,6 +120,7 @@ height: 100%;
}
.eventStats {
padding-left: 0;
z-index: 1;
/* margin-right: 5px; */
}

View File

@@ -103,7 +103,7 @@ switch ( $_COOKIE['zmBandwidth'] ) {
define( 'ZM_WEB_REFRESH_IMAGE', ZM_WEB_H_REFRESH_IMAGE ); // How often the watched image is refreshed (if not streaming)
define( 'ZM_WEB_REFRESH_STATUS', ZM_WEB_H_REFRESH_STATUS ); // How often the little status frame refreshes itself in the watch window
define( 'ZM_WEB_REFRESH_EVENTS', ZM_WEB_H_REFRESH_EVENTS ); // How often the event listing is refreshed in the watch window, only for recent events
define( 'ZM_WEB_REFRESH_LOGS', ZM_WEB_H_REFRESH_LOGS ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_REFRESH_LOGS', defined('ZM_WEB_H_REFRESH_LOGS') ? ZM_WEB_H_REFRESH_LOGS : 0 ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_CAN_STREAM', ZM_WEB_H_CAN_STREAM ); // Override the automatic detection of browser streaming capability
define( 'ZM_WEB_STREAM_METHOD', ZM_WEB_H_STREAM_METHOD ); // Which method should be used to send video streams to your browser
define( 'ZM_WEB_DEFAULT_SCALE', ZM_WEB_H_DEFAULT_SCALE ); // What the default scaling factor applied to 'live' or 'event' views is (%)
@@ -123,7 +123,7 @@ switch ( $_COOKIE['zmBandwidth'] ) {
define( 'ZM_WEB_REFRESH_IMAGE', ZM_WEB_M_REFRESH_IMAGE ); // How often the watched image is refreshed (if not streaming)
define( 'ZM_WEB_REFRESH_STATUS', ZM_WEB_M_REFRESH_STATUS ); // How often the little status frame refreshes itself in the watch window
define( 'ZM_WEB_REFRESH_EVENTS', ZM_WEB_M_REFRESH_EVENTS ); // How often the event listing is refreshed in the watch window, only for recent events
define( 'ZM_WEB_REFRESH_LOGS', ZM_WEB_M_REFRESH_LOGS ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_REFRESH_LOGS', defined('ZM_WEB_M_REFRESH_LOGS') ? ZM_WEB_M_REFRESH_LOGS : 0 ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_CAN_STREAM', ZM_WEB_M_CAN_STREAM ); // Override the automatic detection of browser streaming capability
define( 'ZM_WEB_STREAM_METHOD', ZM_WEB_M_STREAM_METHOD ); // Which method should be used to send video streams to your browser
define( 'ZM_WEB_DEFAULT_SCALE', ZM_WEB_M_DEFAULT_SCALE ); // What the default scaling factor applied to 'live' or 'event' views is (%)
@@ -143,7 +143,7 @@ switch ( $_COOKIE['zmBandwidth'] ) {
define( 'ZM_WEB_REFRESH_IMAGE', ZM_WEB_L_REFRESH_IMAGE ); // How often the watched image is refreshed (if not streaming)
define( 'ZM_WEB_REFRESH_STATUS', ZM_WEB_L_REFRESH_STATUS ); // How often the little status frame refreshes itself in the watch window
define( 'ZM_WEB_REFRESH_EVENTS', ZM_WEB_L_REFRESH_EVENTS ); // How often the event listing is refreshed in the watch window, only for recent events
define( 'ZM_WEB_REFRESH_LOGS', ZM_WEB_L_REFRESH_LOGS ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_REFRESH_LOGS', defined('ZM_WEB_L_REFRESH_LOGS') ? ZM_WEB_L_REFRESH_LOGS : 0 ); // How often (in seconds) the listing is refreshed in the log window
define( 'ZM_WEB_CAN_STREAM', ZM_WEB_L_CAN_STREAM ); // Override the automatic detection of browser streaming capability
define( 'ZM_WEB_STREAM_METHOD', ZM_WEB_L_STREAM_METHOD ); // Which method should be used to send video streams to your browser
define( 'ZM_WEB_DEFAULT_SCALE', ZM_WEB_L_DEFAULT_SCALE ); // What the default scaling factor applied to 'live' or 'event' views is (%)

View File

@@ -615,7 +615,10 @@ function submitThisForm(param = null) {
// Let's hide the old filter so that it doesn't appear during the transfer...
filter.style.display = 'none';
// We return the filter to its place in the form, since in the left side menu the filter should always be inside the form.
form.prepend(filter);
// Skip if filter is already an ancestor of form (e.g. console: #fbpanel > #monitorFiltersForm), which would cause HierarchyRequestError.
if (!filter.contains(form)) {
form.prepend(filter);
}
}
if (param && typeof param === 'string') { //ON WATCH PAGE WHEN SELECTING A MONITOR, the object is transferred as PARAM!!!
var uri = "?" + $j(form).serialize() + param;

View File

@@ -354,9 +354,14 @@ if (file_exists($Event->Path().'/objdetect.jpg')) {
<div id="zoompan" class="zoompan">
<?php
if ($video_tag) {
// Use HLS byte-range playback if m3u8 manifest exists on disk
$has_hls = (($codec == 'MP4HLS') || str_ends_with($Event->DefaultVideo(), '.m3u8'))
&& file_exists($Event->Path() . '/index.m3u8');
// Prefer HLS byte-range playback when the manifest exists on disk and the
// user picked MP4HLS / auto. Explicit MP4 must stay native ("play the mp4
// file directly"); explicit MJPEG never reaches here because $video_tag is
// false for it. DefaultVideo's extension is deliberately not consulted —
// in-progress events have DefaultVideo='index.m3u8' from the constructor,
// and using that as a signal would override the explicit MP4 choice.
$has_hls = file_exists($Event->Path() . '/index.m3u8')
&& (($codec == 'MP4HLS') || ($codec == 'auto'));
if ($has_hls) {
$Server = $Event->Server();
$hlsSrc = $Server->PathToIndex() . '?view=view_hls&amp;eid=' . $Event->Id();
@@ -402,7 +407,11 @@ if ($video_tag) {
autoplay: true,
preload: 'auto',
playbackRates: rates,
liveui: <?php echo $has_hls && !$Event->EndDateTime() ? 'true' : 'false' ?>,
// liveui replaces the seekbar with a live-edge-only control,
// which makes it impossible to scrub back through the already-
// recorded portion of an in-progress event. Always false so the
// standard seekbar is rendered.
liveui: false,
liveTracker: {
trackingThreshold: 0
}

View File

@@ -57,16 +57,19 @@ $content = file_get_contents($m3u8_path);
$Server = $Event->Server();
$base_url = $Server->PathToIndex();
// Replace bare URLs with full paths including auth
// Replace bare relative segment URLs with full paths including auth.
// The m3u8 has lines like "index.php?view=view_video&eid=N&file=F" — capture
// only the query string (after "index.php?") so the replacement doesn't emit
// "/zm/index.php?index.php?view=…".
$content = preg_replace(
'/^(index\.php\?.+)$/m',
'/^index\.php\?(.+)$/m',
$base_url . '?$1' . $auth_query,
$content
);
// Also fix the EXT-X-MAP URI
// Also fix the EXT-X-MAP URI (initialization segment) the same way.
$content = preg_replace(
'/URI="(index\.php\?[^"]+)"/m',
'/URI="index\.php\?([^"]+)"/m',
'URI="' . $base_url . '?$1' . $auth_query . '"',
$content
);

View File

@@ -87,7 +87,10 @@ if ( ! ($fh = @fopen($path, 'rb') ) ) {
header('HTTP/1.0 404 Not Found');
die();
}
$filename = ($mode == 'mp4') ? basename($path) : (($Event) ? $Event->DefaultVideo() : '');
// Always derive the filename from the resolved $path: after the m3u8 fallback
// above, $path can point at an mp4 even when DefaultVideo is 'index.m3u8', so
// reporting DefaultVideo would advertise a manifest while serving mp4 bytes.
$filename = basename($path);
$size = filesize($path);
$begin = 0;