mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-06-23 13:09:23 -04:00
Merge branch 'master' into patch-401263
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -120,6 +120,7 @@ height: 100%;
|
||||
}
|
||||
.eventStats {
|
||||
padding-left: 0;
|
||||
z-index: 1;
|
||||
/* margin-right: 5px; */
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (%)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user