// // ZoneMinder Stream Class Implementation, $Date$, $Revision$ // Copyright (C) 2001-2008 Philip Coombes // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // #include "zm_stream.h" #include "zm_box.h" #include "zm_monitor.h" #include "zm_signal.h" #include #include #include #include #include constexpr Seconds StreamBase::MAX_STREAM_DELAY; constexpr Milliseconds StreamBase::MAX_SLEEP; StreamBase::~StreamBase() { delete vid_stream; delete[] temp_img_buffer; closeComms(); } bool StreamBase::loadMonitor(int p_monitor_id) { monitor_id = p_monitor_id; if ( !(monitor or (monitor = Monitor::Load(monitor_id, false, Monitor::QUERY))) ) { Error("Unable to load monitor id %d for streaming", monitor_id); return false; } if (monitor->Capturing() == Monitor::CAPTURING_NONE) { Info("Monitor %d has capturing == NONE. Will not be able to connect to it.", monitor_id); return false; } // Hold monitor_mutex during disconnect/connect to prevent the command // processor thread from accessing shared memory while it is remapped. std::lock_guard lck(monitor_mutex); if (monitor->isConnected()) { monitor->disconnect(); } if (!monitor->connect()) { Info("Unable to connect to monitor id %d for streaming", monitor_id); monitor->disconnect(); // If we couldn't connect, it might be due to size mismatch in shm. Need to reload if ( !(monitor = Monitor::Load(monitor_id, false, Monitor::QUERY))) Error("Unable to reload monitor id %d for streaming", monitor_id); return false; } return true; } bool StreamBase::checkInitialised() { if (!monitor) { Error("Cannot stream, not initialised"); return false; } if (monitor->Capturing() == Monitor::CAPTURING_NONE) { Info("Monitor %d has capturing == NONE. Will not be able to connect to it.", monitor_id); return false; } if (!monitor->ShmValid()) { Debug(1, "Monitor shm is not connected"); return false; } if ((monitor->GetType() == Monitor::FFMPEG) and (monitor->Decoding() == Monitor::DECODING_NONE) ) { Debug(1, "Monitor is not decoding."); return false; } return true; } void StreamBase::updateFrameRate(double fps) { if ( (fps < 0) || !fps || std::isinf(fps) ) { Debug(1, "Zero or negative fps %f in updateFrameRate. Setting frame_mod=1 and effective_fps=0.0", fps); effective_fps = 0.0; base_fps = 0.0; return; } // Snapshot once so the loop below cannot see maxfps change underneath it. const double max_fps = maxfps.load(); const int rate = replay_rate.load(); base_fps = fps; effective_fps = (base_fps*abs(rate))/ZM_RATE_BASE; frame_mod = 1; Debug(3, "FPS:%.2f, MaxFPS:%.2f, BaseFPS:%.2f, EffectiveFPS:%.2f, FrameMod:%d, replay_rate(%d)", fps, max_fps, base_fps, effective_fps, frame_mod, rate); if (max_fps > 0.0) { // Min frame repeat? // We want to keep the frame skip easy... problem is ... if effective = 31 and max = 30 then we end up with 15.5 fps. while ( (int)effective_fps > (int)max_fps ) { effective_fps /= 2.0; frame_mod *= 2; Debug(3, "Changing fps to be < max %.2f EffectiveFPS:%.2f, FrameMod:%d", max_fps, effective_fps, frame_mod); } } } // void StreamBase::updateFrameRate(double fps) void StreamBase::checkCommandQueue() { while (!zm_terminate) { // Update modified time of the socket .lock file so that we can tell which ones are stale. if (now - last_comm_update > Hours(1)) { touch(sock_path_lock); last_comm_update = now; } if (sd >= 0) { CmdMsg msg; memset(&msg, 0, sizeof(msg)); int nbytes = recvfrom(sd, &msg, sizeof(msg), 0, /*MSG_DONTWAIT*/ 0, 0); if (nbytes < 0) { if (errno != EAGAIN) { Error("recvfrom(), errno = %d, error = %s", errno, strerror(errno)); } } else { Debug(2, "Message length is (%d)", nbytes); processCommand(&msg); got_command = true; } } else if (connkey) { Warning("No sd in checkCommandQueue, comms not open for connkey %06d?", connkey); } else { // Perfectly valid if only getting a snapshot Debug(1, "No sd in checkCommandQueue, comms not open."); } } // end while !zm_terminate } // end void StreamBase::checkCommandQueue() int StreamBase::preScaleDimensions(int base_width, int base_height, int &width, int &height) const { // Any zoom crops before scaling and needs the full resolution to crop out of. if (zoom.load() != ZM_SCALE_BASE) return 0; const int cur_scale = scale.load(); if (cur_scale == ZM_SCALE_BASE) return 0; const int scaled_width = (base_width * cur_scale) / ZM_SCALE_BASE; const int scaled_height = (base_height * cur_scale) / ZM_SCALE_BASE; // CMD_SCALE stores whatever the client sent, so 0 gets here. Image::Scale // refuses a zero factor, but converting straight to 0x0 would instead put a // negative av_image_get_buffer_size result into an unsigned size and allocate // on it, so leave anything degenerate to the path that already rejects it. if ((scaled_width <= 0) or (scaled_height <= 0)) return 0; width = scaled_width; height = scaled_height; return cur_scale; } Image *StreamBase::prepareImage(Image *image, int pre_scaled_by) { if (pre_scaled_by) { // The caller produced the image at exactly the size to send, folding the // scale into a colour conversion it had to do anyway. Send it untouched. // // The command thread can change scale or zoom between the caller reading // them and this call, in which case this one frame goes out at the scale it // was built for and without any new zoom, and the next frame is built from // the new values. Scaling here instead would apply the scale a second time // (a 50% frame of a 640px source would go out at 160px, not 320px), and // cropping here would do it in the reduced image's coordinates and leave // last_crop in the wrong units for every later frame. refs #3681 Debug(3, "Sending pre-scaled %dx%d, scale %d", image->Width(), image->Height(), pre_scaled_by); last_scale = pre_scaled_by; last_zoom = ZM_SCALE_BASE; return image; } /* zooming should happen before scaling to preserve quality * scale is relative to base dimensions, and represents the rough ratio between desired view size and base dimensions */ bool image_copied = false; // Snapshot the pan/zoom/scale state once. The command thread can change any // of these at any point, and the crop below is derived from all three // together, so re-reading them mid-calculation could mix a new zoom with an // old click position and produce a crop that matches neither. const int cur_zoom = zoom.load(); const int cur_scale = scale.load(); const unsigned short cur_x = x.load(); const unsigned short cur_y = y.load(); if (cur_zoom != 100) { int base_image_width = image->Width(), base_image_height = image->Height(), disp_image_width = image->Width() * cur_scale/ZM_SCALE_BASE, disp_image_height = image->Height() * cur_scale / ZM_SCALE_BASE; /* x and y are scaled by web UI to base dimensions units. * When zooming, we blow up the image by the amount 150 for first zoom, right? 150%, then cut out a base sized chunk * However if we have zoomed before, then we are zooming into the previous cutout * The box stored in last_crop should be in base_image units, So we need to turn x,y into percentages, then apply to last_crop */ if (!last_crop.Hi().x_ && !last_crop.Hi().y_) last_crop = Box({0, 0}, {base_image_width, base_image_height}); double x_percent = static_cast(cur_x * ZM_SCALE_BASE) / base_image_width; double y_percent = static_cast(cur_y * ZM_SCALE_BASE) / base_image_height; Debug(2, "click percent %dx%d => %.2fx%.2f", cur_x, cur_y, x_percent, y_percent); // If we were previously zoomed in, then the coordinate percentages are into the crop, so calculate the click coordinates in base image int crop_x = last_crop.Lo().x_ + (x_percent * last_crop.Width() / ZM_SCALE_BASE); int crop_y = last_crop.Lo().y_ + (y_percent * last_crop.Height() / ZM_SCALE_BASE); Debug(2, "crop click %dx%d => %dx%d out of %dx%d", cur_x, cur_y, crop_x, crop_y, last_crop.Width(), last_crop.Height()); int zoom_image_width = base_image_width * cur_zoom / ZM_SCALE_BASE, zoom_image_height = base_image_height * cur_zoom / ZM_SCALE_BASE, click_x = crop_x * cur_zoom / ZM_SCALE_BASE, click_y = crop_y * cur_zoom / ZM_SCALE_BASE; Debug(2, "adjusted click %dx%d * %d zoom => %dx%d out of %dx%d", cur_x, cur_y, cur_zoom, click_x, click_y, zoom_image_width, zoom_image_height); // These can go out of image. Resulting size will be less than base image. That's ok. // We don't want to center it, we want to keep the relative offset from center where the click is. int left_dist = base_image_width * x_percent/ZM_SCALE_BASE; int top_dist = base_image_height * y_percent/ZM_SCALE_BASE; Debug(2, "Dest at %d,%d", left_dist, top_dist); int lo_x = click_x - left_dist; int hi_x = lo_x + base_image_width; Debug(2, "hi_x = lo_x %d + base_image_width %d - left_dist %d = %d", lo_x, base_image_width, left_dist, hi_x); int lo_y = click_y - top_dist; int hi_y = lo_y + base_image_height; Debug(2, "hi_y = lo_y %d + base_image_h %d - top_dist %d = %d", lo_y, base_image_height, top_dist, hi_y); int amount_to_shrink_y = 0; if (lo_x < 0) { amount_to_shrink_y = (-1 * lo_x) * base_image_height / base_image_width; lo_x = 0; } else if (hi_x >= zoom_image_width) { amount_to_shrink_y = (hi_x - zoom_image_width) * base_image_height / base_image_width; hi_x = zoom_image_width - 1; } Debug(1, "Shrinking y by %d from %d,%d to %d,%d", amount_to_shrink_y, lo_y, hi_y, lo_y+(amount_to_shrink_y/2), hi_y-(amount_to_shrink_y/2)); if (amount_to_shrink_y) { lo_y += amount_to_shrink_y/2; hi_y -= amount_to_shrink_y/2; } int amount_to_shrink_x = 0; if (lo_y < 0) { amount_to_shrink_x = (-1 * lo_y) * base_image_width / base_image_height; Debug(1, "%d to %d = %d", lo_y, -1*lo_y, amount_to_shrink_x); lo_y = 0; } else if (hi_y >= zoom_image_height) { amount_to_shrink_x = (hi_y - zoom_image_height) * base_image_width / base_image_height; hi_y = zoom_image_height - 1; } Debug(1, "Shrinking x by %d from %d,%d to %d,%d", amount_to_shrink_x, lo_x, hi_x, lo_x+(amount_to_shrink_x/2), hi_x-(amount_to_shrink_x/2)); if (amount_to_shrink_x) { lo_x += amount_to_shrink_x/2; hi_x -= amount_to_shrink_x/2; } Debug(3, "Cropping to %d,%d -> %d,%d %dx%din blown up image", lo_x, lo_y, hi_x, hi_y, hi_x-lo_x, hi_y-lo_y); // Scaled back to base_image dimensions last_crop = Box({lo_x*ZM_SCALE_BASE/zoom, lo_y*ZM_SCALE_BASE/zoom}, {hi_x*ZM_SCALE_BASE/zoom, hi_y*ZM_SCALE_BASE/zoom}); Debug(3, "Cropping to %d,%d -> %d,%d", last_crop.Lo().x_, last_crop.Lo().y_, last_crop.Hi().x_, last_crop.Hi().y_); if ( !image_copied ) { static Image copy_image; copy_image.Assign(*image); image = ©_image; image_copied = true; } image->Crop(last_crop); image->Scale(disp_image_width, disp_image_height); } else if (cur_scale != ZM_SCALE_BASE) { Debug(3, "scaling by %d from %dx%d", cur_scale, image->Width(), image->Height()); static Image copy_image; copy_image.Assign(*image); image = ©_image; image_copied = true; image->Scale(cur_scale); } Debug(3, "Sending %dx%d", image->Width(), image->Height()); // Record what was actually rendered, not whatever the command thread may // have set since. last_scale = cur_scale; last_zoom = cur_zoom; last_x = cur_x; last_y = cur_y; return image; } // end Image *StreamBase::prepareImage(Image *image, int pre_scaled_by) bool StreamBase::sendTextFrame(const char *frame_text) { int width = 640; int height = 480; int colours = ZM_COLOUR_RGB32; int subpixelorder = ZM_SUBPIX_ORDER_RGBA; int labelsize = 2; if (monitor) { width = monitor->Width(); height = monitor->Height(); colours = monitor->Colours(); subpixelorder = monitor->SubpixelOrder(); labelsize = monitor->LabelSize(); } Debug(2, "Sending %dx%dx%dx%d * %d scale text frame '%s'", width, height, colours, subpixelorder, scale.load(), frame_text); Image image(width, height, colours, subpixelorder); image.Clear(); image.Annotate(frame_text, image.centreCoord(frame_text, labelsize), labelsize); if (scale != 100) { image.Scale(scale); Debug(2, "Scaled to %dx%d", image.Width(), image.Height()); } if (type == STREAM_MPEG) { if (!vid_stream) { vid_stream = new VideoStream("pipe:", format, bitrate, effective_fps, image.Colours(), image.SubpixelOrder(), image.Width(), image.Height()); fprintf(stdout, "Content-Type: %s\r\n\r\n", vid_stream->MimeType()); if (!vid_stream->OpenStream()) { Error("Failed to open video stream"); delete vid_stream; vid_stream = nullptr; return false; } } /* double pts = */ vid_stream->EncodeFrame(image.Buffer(), image.Size()); } else { static unsigned char buffer[ZM_MAX_IMAGE_SIZE]; size_t n_bytes = 0; image.EncodeJpeg(buffer, &n_bytes); if (type == STREAM_JPEG) { if (0 > fputs("--" BOUNDARY "\r\n", stdout)) { Debug(1, "Error sending --" BOUNDARY "\r\n"); return false; } } if (0 > fprintf(stdout, "Content-Type: image/jpeg\r\nContent-Length: %zu\r\n\r\n", n_bytes)) { Debug(1, "Error sending Content-Type: image/jpeg\r\nContent-Length: %zu\r\n\r\n", n_bytes); return false; } int rc = fwrite(buffer, n_bytes, 1, stdout); if (rc != 1) { if (!zm_terminate) Error("Unable to send stream text frame: %d %s", rc, strerror(errno)); return false; } fputs("\r\n\r\n", stdout); fflush(stdout); } last_frame_sent = now; return true; } void StreamBase::openComms() { if ( connkey > 0 ) { // Have to mkdir because systemd is now chrooting and the dir may not exist if ( mkdir(staticConfig.PATH_SOCKS.c_str(), 0755) ) { if ( errno != EEXIST ) { Error("Can't mkdir ZM_PATH_SOCKS %s: %s.", staticConfig.PATH_SOCKS.c_str(), strerror(errno)); } } unsigned int length = snprintf( sock_path_lock, sizeof(sock_path_lock), "%s/zms-%06d.lock", staticConfig.PATH_SOCKS.c_str(), connkey ); if ( length >= sizeof(sock_path_lock) ) { Warning("Socket lock path was truncated."); } Debug(1, "Trying to open the lock on %s", sock_path_lock); // Under systemd, we get chrooted to something like /tmp/systemd-apache-blh/ so the dir may not exist. if ( mkdir(staticConfig.PATH_SOCKS.c_str(), 0755) ) { if ( errno != EEXIST ) { Error("Can't mkdir %s: %s", staticConfig.PATH_SOCKS.c_str(), strerror(errno)); return; } else { Debug(3, "SOCKS dir %s already exists", staticConfig.PATH_SOCKS.c_str() ); } } else { Debug(3, "Success making SOCKS dir %s", staticConfig.PATH_SOCKS.c_str() ); } lock_fd = open(sock_path_lock, O_CREAT|O_WRONLY, S_IRUSR | S_IWUSR); if ( lock_fd < 0 ) { Error("Unable to open sock lock file %s: %s", sock_path_lock, strerror(errno)); lock_fd = -1; } else if ( flock(lock_fd, LOCK_EX) != 0 ) { Error("Unable to lock sock lock file %s: %s", sock_path_lock, strerror(errno)); close(lock_fd); lock_fd = -1; } else { Debug(1, "We have obtained a lock on %s fd: %d", sock_path_lock, lock_fd); } sd = socket(AF_UNIX, SOCK_DGRAM, 0); if ( sd < 0 ) { Fatal("Can't create socket: %s", strerror(errno)); } else { Debug(3, "Have socket %d", sd); } length = snprintf( loc_sock_path, sizeof(loc_sock_path), "%s/zms-%06ds.sock", staticConfig.PATH_SOCKS.c_str(), connkey ); if ( length >= sizeof(loc_sock_path) ) { Warning("Socket path was truncated."); length = sizeof(loc_sock_path)-1; } // Unlink before bind, in case it already exists unlink(loc_sock_path); if ( sizeof(loc_addr.sun_path) < length ) { Error("Not enough space %zu in loc_addr.sun_path for socket file %s", sizeof(loc_addr.sun_path), loc_sock_path); } strncpy(loc_addr.sun_path, loc_sock_path, sizeof(loc_addr.sun_path)); loc_addr.sun_family = AF_UNIX; Debug(3, "Binding to %s", loc_sock_path); if ( ::bind(sd, (struct sockaddr *)&loc_addr, strlen(loc_addr.sun_path)+sizeof(loc_addr.sun_family)+1) < 0 ) { Fatal("Can't bind: %s", strerror(errno)); } snprintf(rem_sock_path, sizeof(rem_sock_path), "%s/zms-%06dw.sock", staticConfig.PATH_SOCKS.c_str(), connkey); strncpy(rem_addr.sun_path, rem_sock_path, sizeof(rem_addr.sun_path)); rem_addr.sun_family = AF_UNIX; struct timeval tv {1,0}; /* 1 Secs Timeout */ setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO,(struct timeval *)&tv, sizeof(struct timeval)); last_comm_update = std::chrono::steady_clock::now(); Debug(3, "comms open at %s", loc_sock_path); } // end if connKey > 0 } // end void StreamBase::openComms() void StreamBase::closeComms() { if ( connkey > 0 ) { if ( sd >= 0 ) { close(sd); sd = -1; } // Remove our command socket, but only while we still hold the lock, and only // if we actually got the lock. // // Note this is zms-s.sock, not the .lock file. A second zms sharing // this connkey is blocked in flock(LOCK_EX) on the .lock file, and the first // thing it does after winning that lock is unlink this very path itself // ("Unlink before bind" in openComms()). It never reads our socket file, so // removing it here only does early what our successor would do anyway. // // Ordering matters: if we unlinked after releasing the lock we could delete // a socket the successor had already bound, leaving it unreachable. // // The lock_fd guard matters because openComms() carries on and binds even // when it fails to take the lock, so without it a lockless zms could delete // a socket belonging to the zms that does hold the lock. Leaking the file in // that case is no worse than the behaviour before this check existed. // // This is worth doing because web/ajax/stream.php uses file_exists() on this // path to decide whether zms is listening. A socket left behind by an exited // zms makes that check succeed, so the sendto() fails with ECONNREFUSED // instead of waiting for the new process to bind, and the command is lost. if ( (lock_fd >= 0) && loc_sock_path[0] && (unlink(loc_sock_path) < 0) && (errno != ENOENT) ) { Warning("Failed to unlink '%s': %s", loc_sock_path, strerror(errno)); } // Don't unlink sock_path_lock: another zms may already be waiting on it. if ( lock_fd >= 0 ) { close(lock_fd); //close it rather than unlock it in case it got deleted. lock_fd = -1; } } } // end void StreamBase::closeComms void StreamBase::reserveTempImgBuffer(size_t size) { if (temp_img_buffer_size < size) { Debug(1, "Resizing image buffer from %zu to %zu", temp_img_buffer_size, size); delete[] temp_img_buffer; temp_img_buffer = new uint8_t[size]; temp_img_buffer_size = size; } }