/** * @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 #include #include #include #include #include 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("type"); Q_ASSERT(protocolType == ONVIF() || protocolType == VAPIX() || protocolType == RTP_RECEIVER()); return obj; } bool isOnvif(const JsonObject &properties) { const QString protocolType = properties.value("type").toLower(); return protocolType == ONVIF(); } bool isVapix(const JsonObject &properties) { const QString protocolType = properties.value("type").toLower(); return protocolType == VAPIX(); } bool isRtpReceiver(const JsonObject &properties) { const QString protocolType = properties.value("type").toLower(); return protocolType == RTP_RECEIVER(); } bool cameraHasMicrophone(const JsonObject &properties) { return properties.valueOr("hasMicrophone", false); } bool cameraHasSpeaker(const JsonObject &properties) { return properties.valueOr("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("username", QStringLiteral("root")); const QString pass = properties.valueOr("password", QStringLiteral("root")); const int port = properties.valueOr(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("port", 5000); int sampleRate = properties.valueOr("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("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