Files
qt5-legatus-legacy/Lts/Services/AudioVideoManagement/audio_video_handler_impl.cpp
T
2026-08-13 18:33:10 +02:00

468 lines
15 KiB
C++
Executable File

/**
* @RDE_HEADER@
* AudioVideoHandlerImpl
* $Id$
* $HeadURL$
* $Author$
* Created by Christian Fröbel, NLSE, 2018-01-09
*/
#include "audio_video_handler_impl.h"
#include "asset.h"
#include "mounting_point.h"
#include "exercise.h"
#include "stream_name_mapping_publisher.h"
#include <AudioVideoManagement/avm.hpp>
#include <InfrastructureManagement/ism.hpp>
#include <LtsUtilsCore/json_object.h>
#include <QString>
#include <QtGlobal>
#include <string>
using LtsUtils::JsonObject;
using namespace AudioVideoManagement;
namespace
{
QString VAPIX() { return QStringLiteral("vapix"); }
QString ONVIF() { return QStringLiteral("onvif"); }
QString RTP_RECEIVER() { return QStringLiteral("rtp_receiver"); } // e.g. EGCU
QString AXIS_AUDIO_CHANNEL() { return QStringLiteral("AXIS_AUDIO_CHANNEL"); }
QString AXIS_VIDEO_CHANNEL() { return QStringLiteral("AXIS_VIDEO_CHANNEL"); }
QString ONVIF_AUDIO_CHANNEL() { return QStringLiteral("ONVIF_AUDIO_CHANNEL"); }
QString ONVIF_VIDEO_CHANNEL() { return QStringLiteral("ONVIF_VIDEO_CHANNEL"); }
QString RTP_ANNOUNCEMENT_CHANNEL() { return QStringLiteral("RTP_ANNOUNCEMENT_CHANNEL"); }
JsonObject jsonFromString(const std::string &assetProperties)
{
JsonObject obj = JsonObject::fromJson(assetProperties);
// type is mandatory and must be either "onvif", "vapix" or "rtc_receiver"
QString protocolType = obj.value<QString>("type");
Q_ASSERT(protocolType == ONVIF() || protocolType == VAPIX() || protocolType == RTP_RECEIVER());
return obj;
}
bool isOnvif(const JsonObject &properties)
{
const QString protocolType = properties.value<QString>("type").toLower();
return protocolType == ONVIF();
}
bool isVapix(const JsonObject &properties)
{
const QString protocolType = properties.value<QString>("type").toLower();
return protocolType == VAPIX();
}
bool isRtpReceiver(const JsonObject &properties)
{
const QString protocolType = properties.value<QString>("type").toLower();
return protocolType == RTP_RECEIVER();
}
bool cameraHasMicrophone(const JsonObject &properties)
{
return properties.valueOr<bool>("hasMicrophone", false);
}
bool cameraHasSpeaker(const JsonObject &properties)
{
return properties.valueOr<bool>("hasSpeaker", false);
}
QString avTypeVideoChannel(const JsonObject &properties)
{
if (isVapix(properties))
return AXIS_VIDEO_CHANNEL();
if (isOnvif(properties))
return ONVIF_VIDEO_CHANNEL();
Q_ASSERT(false);
return AXIS_VIDEO_CHANNEL(); // default
}
QString avTypeAudioChannel(const JsonObject &properties)
{
if (isVapix(properties))
return AXIS_AUDIO_CHANNEL();
if (isOnvif(properties))
return ONVIF_AUDIO_CHANNEL();
if (isRtpReceiver(properties))
return RTP_ANNOUNCEMENT_CHANNEL();
Q_ASSERT(false);
return AXIS_AUDIO_CHANNEL(); // default
}
QString getUrl(const std::string &hostname, const JsonObject &properties, const QString &portString, quint16 defaultPort, bool isAudio)
{
const QString scheme = isAudio || isOnvif(properties) ? QStringLiteral("http") : QStringLiteral("rtsp");
const QString user = properties.valueOr<QString>("username", QStringLiteral("root"));
const QString pass = properties.valueOr<QString>("password", QStringLiteral("root"));
const int port = properties.valueOr<int>(portString, defaultPort);
return QStringLiteral("url=%1://%2:%3@%4:%5;").arg(scheme, user, pass, QString::fromStdString(hostname)).arg(port);
}
QString getAudioUrl(const std::string &hostname, const JsonObject &properties)
{
const bool isAudio = true;
const quint16 defaultPort = 80;
return getUrl(hostname, properties, QStringLiteral("audioPort"), defaultPort, isAudio);
}
QString getVideoUrl(const std::string &hostname, const JsonObject &properties)
{
const bool isVideo = false;
const quint16 defaultPort = isVapix(properties) ? 554 : 80;
return getUrl(hostname, properties, QStringLiteral("port"), defaultPort, isVideo);
}
/**
* For streams of type RTP_ANNOUNCEMENT_CHANNEL.
*/
QString getRtpAccess(const std::string &hostname, const JsonObject &properties)
{
int port = properties.valueOr<int>("port", 5000);
int sampleRate = properties.valueOr<int>("sampleRate", 8000);
Q_ASSERT(sampleRate == 8000 || sampleRate == 16000);
QString sampleRateFlag;
if (sampleRate == 16000)
sampleRateFlag = QStringLiteral("g16kHz=1;");
return QStringLiteral("rtpoutaddress=%1:%2;").arg(QString::fromStdString(hostname)).arg(port) + sampleRateFlag;
}
QString avRootName() { return QStringLiteral("ECRS"); }
QString avController() { return QStringLiteral("EXCAVS01"); }
QString recordingString(bool hasRecordingOn_, bool usePrealarm)
{
QString result = QLatin1String("mode=1;");
if (hasRecordingOn_)
{
result.append(QStringLiteral("recording=%1@%2;").arg(avRootName(), avController()));
if (usePrealarm)
result.append(QStringLiteral("recordcontrol=prealarm;"));
}
return result;
}
bool isMountingPointOfInterest(const InfrastructureManagement::InstrumentationAsset &asset)
{
return (asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::IndoorCamera ||
asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::OutdoorCamera ||
asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::AnnouncementUnit);
}
bool hasRecordingOn(const JsonObject &mountingPointProperties)
{
return mountingPointProperties.valueOr<bool>("record", false);
}
Stream toDds(const AvmDeviceConfigEx &dc, int32_t mountintPointId)
{
Stream stream;
stream.id(dc.id);
stream.name(dc.name.toStdString());
stream.type(dc.streamType);
stream.mountingPointId(mountintPointId);
return stream;
}
} // anonymous namespace
namespace AudioVideoManagement
{
AvmDeviceConfigEx::AvmDeviceConfigEx(const AvmDeviceConfig &dc, StreamType streamType_)
: AvmDeviceConfig(dc)
, streamType(streamType_)
{}
Streams::Streams(Asset *asset_, const AvmDeviceConfigEx &first_)
: asset(asset_)
, first(first_)
{
Q_ASSERT(asset_);
}
Streams::Streams(Asset *asset_, const AvmDeviceConfigEx &first_, const AvmDeviceConfigEx &second_)
: asset(asset_)
, first(first_)
, second(second_)
{
Q_ASSERT(asset_);
Q_ASSERT(first_.name == second_.name);
Q_ASSERT(first.streamType == StreamType::Video);
}
int32_t Streams::assetId() const
{
return asset->data().id();
}
int32_t Streams::mountingPointId() const
{
return asset->mountingPoint()->data().id();
}
AudioVideoHandlerImpl::AudioVideoHandlerImpl(Exercise *exercise, IDbProxy *dbProxy, IDtsProxy *dtsProxy)
: m_exContext(exercise->context())
, m_exercise(exercise)
, m_dbProxy(dbProxy)
, m_dtsProxy(dtsProxy)
, m_logger(getLogger("AudioVideoHandler"))
{
Q_ASSERT(exercise);
Q_ASSERT(dbProxy);
Q_ASSERT(dtsProxy);
connect(m_exercise, &Exercise::assetAssigned, this, &AudioVideoHandlerImpl::onAssetAssigned);
connect(m_exercise, &Exercise::assetUnassigned, this, &AudioVideoHandlerImpl::onAssetUnassigned);
}
/**
* Create one or two AvmDeviceConfigEx structs depending on the type of device:
* - For AnnouncementUnits create only one for the audio stream.
* - For IndoorCamera/OutdoorCamera create one for the video stream and
* another one for the audio stream if the camera has microphone or speaker.
* The id and link properties are not set (set to 0).
*/
Streams AudioVideoHandlerImpl::createStreams(Asset *asset_)
{
Q_ASSERT(asset_);
Database::AvmDeviceConfig dc;
dc.context = m_exContext.toString();
dc.controller = avController();
dc.name = internalStreamName(asset_->mountingPoint()->data().id());
const InfrastructureManagement::InstrumentationAsset &asset = asset_->data();
const JsonObject assetProperties = jsonFromString(asset.properties());
const JsonObject mpProperties = JsonObject::fromJson(asset_->mountingPoint()->data().properties());
if (asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::AnnouncementUnit)
{
dc.definition = deviceDefinitionAnnouncementUnit(asset.hostname(), assetProperties, mpProperties);
dc.type = avTypeAudioChannel(assetProperties);
return {asset_, {dc, StreamType::AudioOut}};
}
Q_ASSERT(asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::IndoorCamera ||
asset.mountingPointTypeId() == InfrastructureManagement::MountingPointType::OutdoorCamera);
dc.definition = deviceDefinitionCameraVideo(asset.hostname(), assetProperties, mpProperties);
dc.type = avTypeVideoChannel(assetProperties);
if (!cameraHasMicrophone(assetProperties) && !cameraHasSpeaker(assetProperties))
return {asset_, {dc, StreamType::Video}};
// additional entry for Microphone or Speaker (audio)
Database::AvmDeviceConfig dcAudio = dc;
StreamType audioStreamType;
if (cameraHasMicrophone(assetProperties) && cameraHasSpeaker(assetProperties))
audioStreamType = StreamType::AudioInOut;
else if (cameraHasMicrophone(assetProperties))
audioStreamType = StreamType::AudioIn;
else
audioStreamType = StreamType::AudioOut;
dcAudio.definition = deviceDefinitionCameraAudio(asset.hostname(), assetProperties, mpProperties);
dcAudio.type = avTypeAudioChannel(assetProperties);
return {asset_, {dc, StreamType::Video}, {dcAudio, audioStreamType}};
}
void AudioVideoHandlerImpl::onAssetAssigned(Asset *asset)
{
Q_ASSERT(asset);
Q_ASSERT(asset->isMounted());
if (!m_isActive)
return;
if (!isMountingPointOfInterest(asset->data()))
return;
connect(asset, &Asset::changed, this, &AudioVideoHandlerImpl::onAssetChanged);
connect(asset->mountingPoint(), &MountingPoint::changed, this, &AudioVideoHandlerImpl::onMountingPointChanged);
addDeviceConfigToDb(asset);
}
/**
* @attention asset will be deleted after the method returns!
* This slot must not be connected using QueuedConnection.
*/
void AudioVideoHandlerImpl::onAssetUnassigned(Asset *asset)
{
Q_ASSERT(asset);
Q_ASSERT(asset->isMounted());
if (!m_isActive)
return;
if (!isMountingPointOfInterest(asset->data()))
return;
removeDeviceConfigFromDb(asset);
asset->disconnect(this);
asset->mountingPoint()->disconnect(this);
}
void AudioVideoHandlerImpl::onAssetChanged(Asset *asset)
{
Q_ASSERT(m_isActive);
Q_ASSERT(asset);
DEBUG_LOG(m_logger) << m_exContext << ": Asset changed: " << asset->data().name() << "(" << asset->data().id() << ")";
updateDeviceConfig(asset);
}
void AudioVideoHandlerImpl::onMountingPointChanged(MountingPoint *mountingPoint)
{
Q_ASSERT(m_isActive);
Q_ASSERT(mountingPoint);
DEBUG_LOG(m_logger) << m_exContext << ": MountingPoint changed: " << mountingPoint->data().name() << "(" << mountingPoint->data().id() << ")";
updateDeviceConfig(mountingPoint->asset());
}
void AudioVideoHandlerImpl::onContextActivated()
{
m_isActive = true;
for (auto asset: m_exercise->assets())
onAssetAssigned(asset);
}
void AudioVideoHandlerImpl::onContextDeactivated()
{
for (auto asset: m_exercise->assets())
onAssetUnassigned(asset);
m_isActive = false;
}
QString AudioVideoHandlerImpl::internalStreamName(int32_t mountingPointId)
{
return QStringLiteral("av_%1").arg(QString::number(mountingPointId), 4, QLatin1Char('0'));
}
QString AudioVideoHandlerImpl::deviceDefinitionAnnouncementUnit(const std::string &hostname, const JsonObject &assetProperties, const JsonObject &mountingPointProperties)
{
// Shall Announcement Units also be run in prealarm mode?
const bool noPrealarm = false;
const QString recording = recordingString(hasRecordingOn(mountingPointProperties), noPrealarm);
if (isRtpReceiver(assetProperties))
return getRtpAccess(hostname, assetProperties) + recording;
// AXIS or ONVIF
return getAudioUrl(hostname, assetProperties) + QStringLiteral("codec=aac;") + recording;
}
QString AudioVideoHandlerImpl::deviceDefinitionCameraVideo(const std::string &hostname, const JsonObject &assetProperties, const JsonObject &mountingPointProperties)
{
const bool usePrealarm = true;
QString definition = getVideoUrl(hostname, assetProperties) + recordingString(hasRecordingOn(mountingPointProperties), usePrealarm);
// Special settings needed for ONVIF-camera used in FAT.
// Different ONVIF-devices wound need different settings here.
if (isOnvif(assetProperties))
definition.append(QStringLiteral("eventtype=1;eventrelaymapping=AlarmOut_1:AlarmOut_0;"));
return definition;
}
QString AudioVideoHandlerImpl::deviceDefinitionCameraAudio(const std::string &hostname, const JsonObject &assetProperties, const JsonObject &mountingPointProperties)
{
const bool usePrealarm = true;
QString definition = getAudioUrl(hostname, assetProperties) + recordingString(hasRecordingOn(mountingPointProperties), usePrealarm);
// Special settings needed for ONVIF-camera used in FAT.
// Different ONVIF-devices would need different settings here.
if (isOnvif(assetProperties))
definition.append(QStringLiteral("eventtype=0;backchannel=2;"));
return definition;
}
void AudioVideoHandlerImpl::addDeviceConfigToDb(Asset *asset_)
{
Q_ASSERT(asset_);
Streams entries = createStreams(asset_);
m_dbProxy->insert(entries);
ddsPublish(entries);
emit avDbReloadNeeded();
}
void AudioVideoHandlerImpl::updateDeviceConfig(Asset *asset)
{
Q_ASSERT(asset);
Streams oldStreams = m_dbProxy->findStreams(asset);
Streams newStreams = createStreams(asset);
m_dbProxy->update(newStreams);
ddsUpdate(oldStreams, newStreams);
emit avDbReloadNeeded();
}
void AudioVideoHandlerImpl::removeDeviceConfigFromDb(Asset *asset)
{
Streams oldStreams = m_dbProxy->findStreams(asset);
m_dbProxy->remove(asset);
ddsRemove(oldStreams);
emit avDbReloadNeeded();
}
void AudioVideoHandlerImpl::ddsPublish(const Streams &streams)
{
int32_t mountingPointId = streams.mountingPointId();
m_dtsProxy->publish(toDds(streams.first, mountingPointId));
if (streams.second)
m_dtsProxy->publish(toDds(*streams.second, mountingPointId));
}
void AudioVideoHandlerImpl::ddsUpdate(const Streams &oldStreams, const Streams &newStreams)
{
Q_ASSERT(newStreams.asset == oldStreams.asset);
Q_ASSERT(newStreams.first.id == oldStreams.first.id);
int32_t mountingPointId = newStreams.mountingPointId();
m_dtsProxy->publish(toDds(newStreams.first, mountingPointId));
if (newStreams.second)
{
if (oldStreams.second)
Q_ASSERT(newStreams.second->id == oldStreams.second->id);
m_dtsProxy->publish(toDds(*newStreams.second, mountingPointId));
}
else
{
if (oldStreams.second)
m_dtsProxy->remove(oldStreams.second->id);
}
}
void AudioVideoHandlerImpl::ddsRemove(const Streams &streams)
{
m_dtsProxy->remove(streams.first.id);
if (streams.second)
m_dtsProxy->remove(streams.second->id);
}
} // namespace AudioVideoManagement