/** * @RDE_HEADER@ * AudioVideoHandlerImplTest * $Id$ * $HeadURL$ * $Author$ * Created by Christian Fröbel, NLSE, 2018-01-09 */ #include "audio_video_handler_impl_test.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace AudioVideoManagement; using namespace InfrastructureManagement; using LtsUtils::JsonObject; namespace { int streamCount(const Streams &streams) { if (streams.second) return 2; return 1; } class DbMock : public AudioVideoHandlerImpl::IDbProxy { public: const Streams &findStreams(AssetId id) const override { auto found = m_cache.find(id); Q_ASSERT(found != m_cache.end()); return found->second; } const Streams &findStreams(Asset *asset) override { Q_ASSERT(asset); return findStreams(asset->data().id()); } bool isEmpty() const { return m_cache.empty(); } int deviceCount() const { return m_cache.size(); } int streamCount() const { int count = 0; for (const auto &streams: m_cache) count += ::streamCount(streams.second); return count; } void insert(Streams &streams) override { streams.first.id = ++m_nextId; if (streams.second) streams.second->id = ++m_nextId; link(streams); auto insertResult = m_cache.insert(std::make_pair(streams.assetId(), streams)); Q_ASSERT(insertResult.second); } /** * We simply maintain the ids or give a new id to a possibly new entry. */ void update(Streams &newStreams) override { auto found = m_cache.find(newStreams.assetId()); Q_ASSERT(found != m_cache.end()); const Streams oldStreams = found->second; newStreams.first.id = oldStreams.first.id; if (newStreams.second) { if (oldStreams.second) newStreams.second->id = oldStreams.second->id; else newStreams.second->id = ++m_nextId; } link(newStreams); found->second = newStreams; // update cache } void remove(Asset *asset) override { Q_ASSERT(asset); auto count = m_cache.erase(asset->data().id()); Q_ASSERT(count == 1); } private: void link(Streams& streams) { if (!streams.second) { streams.first.link = 0; return; } streams.first.link = streams.second->id; streams.second->link = streams.first.id; } int m_nextId{0}; std::map m_cache; }; class DtsSpy : public AudioVideoHandlerImpl::IDtsProxy { public: void publish(const Stream &stream) override { m_published.push_back(stream); } void remove(uint64_t streamId) override { m_removed.push_back(streamId); } bool isEmpty() const { return m_published.empty() && m_removed.empty(); } void reset() { m_published.clear(); m_removed.clear(); } std::vector m_published; std::vector m_removed; }; Asset *createAsset(int id, MountingPointType mpType, const std::string &properties) { InstrumentationAsset data; data.id(id); data.name(QStringLiteral("ASSET_000%1").arg(id).toUtf8().constData()); data.hostname(QStringLiteral("host_000%1").arg(id).toUtf8().constData()); data.mountingPointTypeId(mpType); data.properties(properties); return new Asset(data); } MountingPoint *createMountingPoint(int id, MountingPointType mpType) { StructureElement data; data.id(id); data.name(QStringLiteral("MP_000%1").arg(id).toUtf8().constData()); data.displayName(QStringLiteral("display name %1").arg(id).toUtf8().constData()); data.structureElementTypeId(StructureElementType::MountingPoint); data.mountingPointTypeId(mpType); data.properties("{}"); return new MountingPoint(data); } void setProperties(Asset *asset, const std::string &newProperties) { Q_ASSERT(asset); InstrumentationAsset data = asset->data(); data.properties(newProperties); asset->update(data); } void setDisplayName(MountingPoint *mp, const std::string &newDisplayName) { Q_ASSERT(mp); StructureElement data = mp->data(); data.displayName(newDisplayName); mp->update(data); } void setRecording(MountingPoint *mp, bool on) { Q_ASSERT(mp); StructureElement data = mp->data(); if (on) data.properties(R"({"record": true})"); else data.properties(R"({"record": false})"); mp->update(data); } void mount(Asset *asset, MountingPoint *mp) { Q_ASSERT(asset); InstrumentationAsset data = asset->data(); data.assetStateId(InstrumentationAssetState::Mounted); asset->update(data); asset->mount(mp); } Database::AvmDeviceConfig dcEntry(int64_t id, const std::string &context, const std::string &type, const std::string &name, const std::string &controller, const std::string &definition, int32_t link) { Database::AvmDeviceConfig dc; dc.id = id; dc.context = QString::fromStdString(context); dc.type = QString::fromStdString(type); dc.name = QString::fromStdString(name); dc.controller = QString::fromStdString(controller); dc.definition = QString::fromStdString(definition); dc.link = link; return dc; } bool operator==(const Database::AvmDeviceConfig &lhs, const Database::AvmDeviceConfig &rhs) { if (lhs.id != rhs.id) return false; if (lhs.context != rhs.context) return false; if (lhs.type != rhs.type) return false; if (lhs.name != rhs.name) return false; if (lhs.controller != rhs.controller) return false; if (lhs.definition != rhs.definition) return false; if (lhs.link != rhs.link) return false; return true; } } // anonymous namespace namespace AudioVideoManagement { AudioVideoHandlerImplTest::AudioVideoHandlerImplTest() = default; AudioVideoHandlerImplTest::~AudioVideoHandlerImplTest() = default; /** * Create Assets and MountingPoints. * They will be mounted already but left in the pool exercise. */ void AudioVideoHandlerImplTest::init() { m_nextAssetId = 0; m_nextMpId = 0; m_pool.reset(new Exercise(ExerciseContext::defaultContext())); m_exercise.reset(new Exercise(ExerciseContext(42))); m_indoorCam = createAsset(++m_nextAssetId, MountingPointType::IndoorCamera, R"({"type": "vapix"})"); m_outdoorCam = createAsset(++m_nextAssetId, MountingPointType::OutdoorCamera, R"({"type": "vapix"})"); m_announcementUnit = createAsset(++m_nextAssetId, MountingPointType::AnnouncementUnit, R"({"type": "vapix"})"); m_mpIndoor = createMountingPoint(++m_nextMpId, MountingPointType::IndoorCamera); m_mpOutdoor = createMountingPoint(++m_nextMpId, MountingPointType::OutdoorCamera); m_mpAnnouncementUnit = createMountingPoint(++m_nextMpId, MountingPointType::AnnouncementUnit); mount(m_indoorCam, m_mpIndoor); mount(m_outdoorCam, m_mpOutdoor); mount(m_announcementUnit, m_mpAnnouncementUnit); m_pool->add(std::unique_ptr(m_indoorCam)); m_pool->add(std::unique_ptr(m_outdoorCam)); m_pool->add(std::unique_ptr(m_announcementUnit)); m_pool->add(std::unique_ptr(m_mpIndoor)); m_pool->add(std::unique_ptr(m_mpOutdoor)); m_pool->add(std::unique_ptr(m_mpAnnouncementUnit)); } void AudioVideoHandlerImplTest::cleanup() { m_pool.reset(); m_exercise.reset(); } /** * Assign a camera to an Exercise that neither has microphone nor speaker. */ void AudioVideoHandlerImplTest::testAssignCameraVideo() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); handler.onContextActivated(); QVERIFY(dbMock.isEmpty()); QVERIFY(dtsSpy.isEmpty()); m_pool->moveMountedTo(m_indoorCam, m_exercise.get(), boost::none); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // verify DB const Streams &streams = dbMock.findStreams(m_indoorCam->data().id()); QVERIFY(!streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0001", "EXCAVS01", "url=rtsp://root:root@host_0001:554;mode=1;", 0)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0001", StreamType::Video, m_mpIndoor->data().id())); } /** * Assign a Camera to an Exercise that has a microphone. */ void AudioVideoHandlerImplTest::testAssignCameraVideoAudioIn() { testAssignCameraVideoAudio(R"({"type": "vapix", "hasMicrophone": true})", StreamType::AudioIn); } /** * Assign a Camera to an Exercise that has a speaker. */ void AudioVideoHandlerImplTest::testAssignCameraVideoAudioOut() { testAssignCameraVideoAudio(R"({"type": "vapix", "hasSpeaker": true})", StreamType::AudioOut); } /** * Assign a Camera to an Exercise that has both, microphone and speaker. */ void AudioVideoHandlerImplTest::testAssignCameraVideoAudioInOut() { testAssignCameraVideoAudio(R"({"type": "vapix", "hasMicrophone": true, "hasSpeaker": true})", StreamType::AudioInOut); } void AudioVideoHandlerImplTest::testAssignCameraVideoAudio(const std::string &camProperties, StreamType expectedStreamType) { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); // Change the Asset's properties before activating the context. setProperties(m_outdoorCam, camProperties); handler.onContextActivated(); m_pool->moveMountedTo(m_outdoorCam, m_exercise.get(), boost::none); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 2); QCOMPARE(static_cast(dtsSpy.m_published.size()), 2); QVERIFY(dtsSpy.m_removed.empty()); // verfiy DB const Streams &streams = dbMock.findStreams(m_outdoorCam->data().id()); QVERIFY(streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0002", "EXCAVS01", "url=rtsp://root:root@host_0002:554;mode=1;", 2)); QVERIFY(*streams.second == dcEntry(2, "ex_42", "AXIS_AUDIO_CHANNEL", "av_0002", "EXCAVS01", "url=http://root:root@host_0002:80;mode=1;", 1)); // verify DDS const Stream &videoStream = dtsSpy.m_published[0]; QCOMPARE(videoStream, Stream(1, "av_0002", StreamType::Video, m_mpOutdoor->data().id())); const Stream &audioStream = dtsSpy.m_published[1]; QCOMPARE(audioStream, Stream(2, "av_0002", expectedStreamType, m_mpOutdoor->data().id())); } /** * Assign an AnnouncementUnit to an exercise. */ void AudioVideoHandlerImplTest::testAssignAnnouncementUnit() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); handler.onContextActivated(); m_pool->moveMountedTo(m_announcementUnit, m_exercise.get(), boost::none); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // verfiy DB const Streams &streams = dbMock.findStreams(m_announcementUnit->data().id()); QVERIFY(!streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_AUDIO_CHANNEL", "av_0003", "EXCAVS01", "url=http://root:root@host_0003:80;codec=aac;mode=1;", 0)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0003", StreamType::AudioOut, m_mpAnnouncementUnit->data().id())); } /** * In this test we assign Assets to the Exercise *before* the Exercise gets activated. */ void AudioVideoHandlerImplTest::testAssignBeforeActivation() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); m_pool->moveMountedTo(m_indoorCam, m_exercise.get(), boost::none); m_pool->moveMountedTo(m_outdoorCam, m_exercise.get(), boost::none); m_pool->moveMountedTo(m_announcementUnit, m_exercise.get(), boost::none); QVERIFY(dbMock.isEmpty()); QVERIFY(dtsSpy.isEmpty()); handler.onContextActivated(); QCOMPARE(dbMock.deviceCount(), 3); QCOMPARE(dbMock.streamCount(), 3); QCOMPARE(static_cast(dtsSpy.m_published.size()), 3); } void AudioVideoHandlerImplTest::testUnassignCameraVideo() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); m_pool->moveMountedTo(m_indoorCam, m_exercise.get(), boost::none); handler.onContextActivated(); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); dtsSpy.reset(); m_exercise->moveMountedTo(m_indoorCam, m_pool.get(), boost::none); QVERIFY(dbMock.isEmpty()); QVERIFY(dtsSpy.m_published.empty()); QCOMPARE(static_cast(dtsSpy.m_removed.size()), 1); QCOMPARE(static_cast(dtsSpy.m_removed[0]), 1); } /** * Update a Camera while it is assigned. */ void AudioVideoHandlerImplTest::testUpdateRemoveAssignedAsset() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); m_pool->moveMountedTo(m_indoorCam, m_exercise.get(), boost::none); handler.onContextActivated(); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // add microphone to Camera => one additional audio stream dtsSpy.reset(); setProperties(m_indoorCam, R"({"type": "vapix", "hasMicrophone": true})"); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 2); QCOMPARE(static_cast(dtsSpy.m_published.size()), 2); // one updated, one new QVERIFY(dtsSpy.m_removed.empty()); // verify DB Streams streams = dbMock.findStreams(m_indoorCam->data().id()); QVERIFY(streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0001", "EXCAVS01", "url=rtsp://root:root@host_0001:554;mode=1;", 2)); QVERIFY(*streams.second == dcEntry(2, "ex_42", "AXIS_AUDIO_CHANNEL", "av_0001", "EXCAVS01", "url=http://root:root@host_0001:80;mode=1;", 1)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0001", StreamType::Video, m_mpIndoor->data().id())); QCOMPARE(dtsSpy.m_published[1], Stream(2, "av_0001", StreamType::AudioIn, m_mpIndoor->data().id())); // remove microphone again => removal of audio stream dtsSpy.reset(); setProperties(m_indoorCam, R"({"type": "vapix", "hasMicrophone": false})"); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); // one updated QCOMPARE(static_cast(dtsSpy.m_removed.size()), 1); // verify DB streams = dbMock.findStreams(m_indoorCam->data().id()); QVERIFY(!streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0001", "EXCAVS01", "url=rtsp://root:root@host_0001:554;mode=1;", 0)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0001", StreamType::Video, m_mpIndoor->data().id())); QCOMPARE(static_cast(dtsSpy.m_removed[0]), 2); // unassign the Camera dtsSpy.reset(); m_exercise->moveMountedTo(m_indoorCam, m_pool.get(), boost::none); QCOMPARE(dbMock.deviceCount(), 0); QVERIFY(dtsSpy.m_published.empty()); QCOMPARE(static_cast(dtsSpy.m_removed.size()), 1); // verify DDS QCOMPARE(static_cast(dtsSpy.m_removed[0]), 1); } /** * Update the MountingPoint of an assigned Asset. */ void AudioVideoHandlerImplTest::testUpdateAssignedMountingPoint() { DbMock dbMock; DtsSpy dtsSpy; AudioVideoHandlerImpl handler(m_exercise.get(), &dbMock, &dtsSpy); m_pool->moveMountedTo(m_indoorCam, m_exercise.get(), boost::none); handler.onContextActivated(); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // Change the displayName of the MountingPoint. // We now distinguish between internal stream name and display name. // Updating the displayName property of a MountingPoint does not change the internal stream name. dtsSpy.reset(); setDisplayName(m_mpIndoor, "updated display name"); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // verify DB Streams streams = dbMock.findStreams(m_indoorCam->data().id()); QVERIFY(!streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0001", "EXCAVS01", "url=rtsp://root:root@host_0001:554;mode=1;", 0)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0001", StreamType::Video, m_mpIndoor->data().id())); // set recording-flag on MountingPoint dtsSpy.reset(); setRecording(m_mpIndoor, true); QCOMPARE(dbMock.deviceCount(), 1); QCOMPARE(dbMock.streamCount(), 1); QCOMPARE(static_cast(dtsSpy.m_published.size()), 1); QVERIFY(dtsSpy.m_removed.empty()); // verify DB streams = dbMock.findStreams(m_indoorCam->data().id()); QVERIFY(!streams.second); QVERIFY(streams.first == dcEntry(1, "ex_42", "AXIS_VIDEO_CHANNEL", "av_0001", "EXCAVS01", "url=rtsp://root:root@host_0001:554;mode=1;recording=ECRS@EXCAVS01;recordcontrol=prealarm;", 0)); // verify DDS QCOMPARE(dtsSpy.m_published[0], Stream(1, "av_0001", StreamType::Video, m_mpIndoor->data().id())); // no change here } void AudioVideoHandlerImplTest::testDeviceDefinitionCameraVideo() { QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraVideo("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=rtsp://root:root@host:554;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraVideo("host", JsonObject::fromJson(R"({"type": "vapix", "username": "user", "password": "1234"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=rtsp://user:1234@host:554;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraVideo("host", JsonObject::fromJson(R"({"type": "vapix", "port": 123})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=rtsp://root:root@host:123;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraVideo("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({"record": true})")), QStringLiteral("url=rtsp://root:root@host:554;mode=1;recording=ECRS@EXCAVS01;recordcontrol=prealarm;")); } void AudioVideoHandlerImplTest::testDeviceDefinitionCameraAudio() { QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraAudio("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://root:root@host:80;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraAudio("host", JsonObject::fromJson(R"({"type": "vapix", "username": "user", "password": "1234"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://user:1234@host:80;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraAudio("host", JsonObject::fromJson(R"({"type": "vapix", "audioPort": 88})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://root:root@host:88;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionCameraAudio("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({"record": true})")), QStringLiteral("url=http://root:root@host:80;mode=1;recording=ECRS@EXCAVS01;recordcontrol=prealarm;")); } void AudioVideoHandlerImplTest::testDeviceDefinitionAnnouncementUnit() { QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionAnnouncementUnit("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://root:root@host:80;codec=aac;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionAnnouncementUnit("host", JsonObject::fromJson(R"({"type": "vapix", "username": "user", "password": "1234"})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://user:1234@host:80;codec=aac;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionAnnouncementUnit("host", JsonObject::fromJson(R"({"type": "vapix", "audioPort": 88})"), JsonObject::fromJson(R"({})")), QStringLiteral("url=http://root:root@host:88;codec=aac;mode=1;")); QCOMPARE(AudioVideoHandlerImpl::deviceDefinitionAnnouncementUnit("host", JsonObject::fromJson(R"({"type": "vapix"})"), JsonObject::fromJson(R"({"record": true})")), QStringLiteral("url=http://root:root@host:80;codec=aac;mode=1;recording=ECRS@EXCAVS01;")); } } // namespace AudioVideoManagement