/*
* This file is part of the ZoneMinder Project. See AUTHORS file for contributors.
*
* 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, see .
*/
#include "zm_catch2.h"
#include "zm_stream.h"
#include "zm_config.h"
#include "zm_image.h"
#include
#include
namespace {
// Minimal concrete StreamBase so the base constructor and destructor can be
// exercised without a monitor, a database or shared memory.
class TestStream : public StreamBase {
public:
void runStream() override {}
// prepareImage and the view state it reads are protected; expose just enough
// to drive it from a test.
Image *prepare(Image *image, int pre_scaled_by = 0) {
return prepareImage(image, pre_scaled_by);
}
int preScale(int base_width, int base_height, int &width, int &height) {
return preScaleDimensions(base_width, base_height, width, height);
}
void setView(int p_scale, int p_zoom) {
scale = p_scale;
zoom = p_zoom;
}
protected:
void processCommand(const CmdMsg *) override {}
};
// Image::Initialise() dereferences config.font_file_location, which is null in
// the unit-test harness (no zm.conf loaded). Give it a non-null value so the
// first Image construction doesn't throw. Same guard as zm_image_linesize.cpp.
void EnsureImageInit() {
if (!config.font_file_location) config.font_file_location = "";
}
bool fd_is_open(int fd) {
return fcntl(fd, F_GETFD) != -1;
}
// Destruct a TestStream with the given connkey and report whether fd 0
// survived. fd 0 is restored either way, so a failure here can't cascade into
// the rest of the suite.
bool stdin_survives_destruction(int connkey) {
REQUIRE(fd_is_open(STDIN_FILENO));
int saved = dup(STDIN_FILENO);
REQUIRE(saved != -1);
{
TestStream stream;
if (connkey) stream.setStreamQueue(connkey);
}
bool survived = fd_is_open(STDIN_FILENO);
if (!survived) REQUIRE(dup2(saved, STDIN_FILENO) != -1);
close(saved);
return survived;
}
} // namespace
TEST_CASE("StreamBase comms teardown") {
SECTION("destructing without openComms() leaves stdin alone") {
// Regression: lock_fd was initialised to 0 rather than -1, so closeComms()
// -- reached from ~StreamBase() -- saw `lock_fd >= 0` and called close(0),
// closing stdin. It only needed connkey > 0 and a runStream() that returned
// before openComms(). MonitorStream::runStream() does exactly that for
// STREAM_SINGLE and when the monitor fails to load, and mode=single URLs
// still carry a connkey.
REQUIRE(stdin_survives_destruction(123456));
}
SECTION("destructing with no connkey leaves stdin alone") {
// closeComms() is a no-op without a connkey, so this held even before the
// fix. Here to pin the guard down.
REQUIRE(stdin_survives_destruction(0));
}
}
TEST_CASE("StreamBase::prepareImage pre-scaled frames") {
// zms used to colour convert an mp4 frame to full size RGBA and then scale
// that down to the requested size, so every streamed frame paid for two
// sws_scale passes and a full frame copy. sws_scale converts and resizes in
// one pass, so the decode side now produces the image at the size being sent
// and tells prepareImage it has already been scaled. refs #3681
EnsureImageInit();
const unsigned int kWidth = 640;
const unsigned int kHeight = 480;
SECTION("a pre-scaled image is passed through untouched") {
TestStream stream;
stream.setView(50, ZM_SCALE_BASE);
Image image(kWidth / 2, kHeight / 2, ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
Image *sent = stream.prepare(&image, 50);
// Same object, not a scaled copy: nothing was resized and nothing copied.
REQUIRE(sent == &image);
REQUIRE(sent->Width() == kWidth / 2);
REQUIRE(sent->Height() == kHeight / 2);
}
SECTION("a full size image is still scaled") {
TestStream stream;
stream.setView(50, ZM_SCALE_BASE);
Image image(kWidth, kHeight, ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
Image *sent = stream.prepare(&image, 0);
REQUIRE(sent != &image); // scaled into the copy, caller's image intact
REQUIRE(image.Width() == kWidth);
REQUIRE(sent->Width() == kWidth / 2);
REQUIRE(sent->Height() == kHeight / 2);
}
SECTION("a scale changed since the decode is not applied a second time") {
// The command thread can change scale after the decode side read it. The
// image in hand is a real 25% frame; scaling it again by the new 50% would
// send 80x60 rather than either the 160x120 it is or the 320x240 now
// wanted. It goes out as built and the next frame uses the new scale.
TestStream stream;
stream.setView(50, ZM_SCALE_BASE);
Image image((kWidth * 25) / 100, (kHeight * 25) / 100,
ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
Image *sent = stream.prepare(&image, 25);
REQUIRE(sent == &image);
REQUIRE(sent->Width() == (kWidth * 25) / 100);
REQUIRE(sent->Height() == (kHeight * 25) / 100);
}
SECTION("a zoom turned on since the decode does not crop the reduced image") {
// Same race, the other field. The zoom branch derives its base geometry and
// its crop from the image's own dimensions, so handing it a half sized frame
// would both halve the output again and record last_crop in the wrong
// coordinate space, which would then be wrong for every later frame.
TestStream stream;
stream.setView(50, 150);
Image image(kWidth / 2, kHeight / 2, ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
Image *sent = stream.prepare(&image, 50);
REQUIRE(sent == &image);
REQUIRE(sent->Width() == kWidth / 2);
REQUIRE(sent->Height() == kHeight / 2);
}
SECTION("unscaled streaming is unaffected") {
TestStream stream;
stream.setView(ZM_SCALE_BASE, ZM_SCALE_BASE);
Image image(kWidth, kHeight, ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
Image *sent = stream.prepare(&image, 0);
REQUIRE(sent == &image);
REQUIRE(sent->Width() == kWidth);
}
SECTION("converting at the target size lands on the size Scale would have") {
// The whole optimisation rests on these agreeing: preScaleDimensions
// computes (dimension * scale) / ZM_SCALE_BASE, which has to match what
// Image::Scale(scale) produces, or pre-scaled frames would go out at a
// different size than they used to.
for (unsigned int scale : {25u, 33u, 50u, 75u, 150u}) {
Image scaled(kWidth, kHeight, ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA);
scaled.Scale(scale);
CAPTURE(scale);
REQUIRE(scaled.Width() == (kWidth * scale) / ZM_SCALE_BASE);
REQUIRE(scaled.Height() == (kHeight * scale) / ZM_SCALE_BASE);
}
}
}
TEST_CASE("StreamBase::preScaleDimensions") {
const int kWidth = 640;
const int kHeight = 480;
int width = -1, height = -1;
SECTION("reports the scaled size when only a scale is set") {
TestStream stream;
stream.setView(50, ZM_SCALE_BASE);
REQUIRE(stream.preScale(kWidth, kHeight, width, height) == 50);
REQUIRE(width == 320);
REQUIRE(height == 240);
}
SECTION("declines while a zoom is active") {
// Zoom crops before scaling and needs the full resolution to crop out of.
TestStream stream;
stream.setView(50, 150);
REQUIRE(stream.preScale(kWidth, kHeight, width, height) == 0);
}
SECTION("declines at the base scale, where there is nothing to fold in") {
TestStream stream;
stream.setView(ZM_SCALE_BASE, ZM_SCALE_BASE);
REQUIRE(stream.preScale(kWidth, kHeight, width, height) == 0);
}
SECTION("declines a scale that would produce a zero dimension") {
// CMD_SCALE stores the two bytes the client sent without validating them,
// so 0 reaches here. Image::Scale(0) refuses the factor; asking swscale for
// a 0x0 frame would instead take a negative buffer size into an unsigned
// field and allocate on it.
TestStream stream;
stream.setView(0, ZM_SCALE_BASE);
REQUIRE(stream.preScale(kWidth, kHeight, width, height) == 0);
// Also when the scale is nonzero but too small to survive the division.
stream.setView(1, ZM_SCALE_BASE);
REQUIRE(stream.preScale(4, 4, width, height) == 0);
}
SECTION("leaves the out params alone when it declines") {
TestStream stream;
stream.setView(0, ZM_SCALE_BASE);
width = kWidth;
height = kHeight;
REQUIRE(stream.preScale(kWidth, kHeight, width, height) == 0);
REQUIRE(width == kWidth);
REQUIRE(height == kHeight);
}
}