Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/build-onvif-video-broker-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build ONVIF Broker (.NET)

on:
push:
branches: [ main ]
paths:
- .github/workflows/build-onvif-video-broker-container.yml
- build/brokers/Dockerfile.onvif-video-broker
- brokers/onvif-video-broker/**
- version.txt
- Makefile
pull_request:
branches: [ main ]
paths:
- .github/workflows/build-onvif-video-broker-container.yml
- build/brokers/Dockerfile.onvif-video-broker
- brokers/onvif-video-broker/**
- version.txt
- Makefile
release:
types:
- published

env:
AKRI_COMPONENT: onvif-video-broker
MAKEFILE_COMPONENT: onvif

jobs:
build-broker:
runs-on: ubuntu-latest
timeout-minutes: 20

# Allow GITHUB_TOKEN to push to GHCR
permissions:
contents: read
packages: write

steps:
- name: Checkout the head commit of the branch
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Get version.txt
id: version-string
run: |
echo "version=$(cat version.txt)" >> $GITHUB_OUTPUT

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
if: github.event_name == 'push' || github.event_name == 'release'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
run: |
make onvif-video-broker PREFIX=ghcr.io/project-akri/akri $(${{ github.event_name != 'pull_request' }} && echo "PUSH=1")
33 changes: 32 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ TIMESTAMP := $(shell date +"%Y%m%d_%H%M%S")
VERSION_LABEL=v$(VERSION)-$(TIMESTAMP)
LABEL_PREFIX ?= $(VERSION_LABEL)

USE_OPENCV_BASE_VERSION = 0.0.11
AMD64_SUFFIX = amd64
ARM32V7_SUFFIX = arm32v7
ARM64V8_SUFFIX = arm64v8

.PHONY: all
all: apps brokers

Expand All @@ -42,11 +47,37 @@ apps: anomaly-detection-app video-streaming-app
--file build/apps/Dockerfile.python-app .

.PHONY: brokers
brokers: udev-video-broker
brokers: udev-video-broker onvif-video-broker

udev-video-broker:
docker buildx build $(COMMON_DOCKER_BUILD_ARGS) \
--tag "$(PREFIX)/$@:$(LABEL_PREFIX)" \
--build-arg EXTRA_CARGO_ARGS="$(if $(BUILD_RELEASE_FLAG), --release)" \
--file build/brokers/Dockerfile.rust \
brokers/$@

# Still use old-ish style for onvif-video-broker as app uses .NET 3.1 that doesn't have multi-arch manifest
onvif-video-broker: onvif-video-broker-multiarch

onvif-video-broker-multiarch: onvif-video-broker-amd64 onvif-video-broker-arm64 onvif-video-broker-arm32
ifeq (1, $(PUSH))
docker buildx imagetools create --tag "$(PREFIX)/onvif-video-broker:$(LABEL_PREFIX)"
endif

ONVIF_BUILDX_PUSH_OUTPUT = type=image,name=$(PREFIX)/onvif-video-broker,push-by-digest=true,name-canonical=true,push=true
ONVIF_BUILDX_ARGS = $(if $(LOAD), --load --tag $(PREFIX)/onvif-video-broker:$(LABEL_PREFIX)) $(if $(PUSH), --output $(ONVIF_BUILDX_PUSH_OUTPUT)) -f build/brokers/Dockerfile.onvif-video-broker

onvif-video-broker-amd64:
ifneq (,or(findstring(amd64,$(PLATFORMS)), findstring(x86_64,$(PLATFORMS))))
docker buildx build $(ONVIF_BUILDX_ARGS) $(if $(PUSH), --iidfile onvif-video-broker.sha-amd64) --build-arg OUTPUT_PLATFORM_TAG=$(USE_OPENCV_BASE_VERSION)-$(AMD64_SUFFIX) --build-arg DOTNET_PUBLISH_RUNTIME=linux-x64 .
endif

onvif-video-broker-arm32:
ifneq (,findstring(arm/v7,$(PLATFORMS)))
docker buildx build $(ONVIF_BUILDX_ARGS) $(if $(PUSH), --iidfile onvif-video-broker.sha-arm32) --build-arg OUTPUT_PLATFORM_TAG=$(USE_OPENCV_BASE_VERSION)-$(ARM32V7_SUFFIX) --build-arg DOTNET_PUBLISH_RUNTIME=linux-arm .
endif

onvif-video-broker-arm64:
ifneq (,or(findstring(aarch64,$(PLATFORMS)),findstring(arm64,$(PLATFORMS))))
docker buildx build $(ONVIF_BUILDX_ARGS) $(if $(PUSH), --iidfile onvif-video-broker.sha-arm64) --build-arg OUTPUT_PLATFORM_TAG=$(USE_OPENCV_BASE_VERSION)-$(ARM64V8_SUFFIX) --build-arg DOTNET_PUBLISH_RUNTIME=linux-arm64 .
endif
198 changes: 198 additions & 0 deletions brokers/onvif-video-broker/Akri.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;

namespace Akri
{
public static class Akri
{
private static string PostSoapRequest(String requestUri, String action, String soapMessage)
{
var request = (HttpWebRequest) WebRequest.CreateDefault(new Uri(requestUri));
request.ContentType = "application/soap+xml; charset=utf-8";
request.Method = HttpMethod.Post.ToString();
request.Headers.Add("SOAPAction", action);
using (var stream = new StreamWriter(request.GetRequestStream(), Encoding.UTF8))
{
stream.Write(soapMessage);
}

Console.WriteLine($"[Akri] ONVIF request {requestUri} {action}");
using (WebResponse requestResponse = request.GetResponse())
{
using (StreamReader responseReader = new StreamReader(requestResponse.GetResponseStream()))
{
return responseReader.ReadToEnd();
}
}
}

private const String MEDIA_WSDL = "http://www.onvif.org/ver10/media/wsdl";
private const String DEVICE_WSDL = "http://www.onvif.org/ver10/device/wsdl";
private const String GET_SERVICE_SOAP = @"<soap:Envelope xmlns:soap=""http://www.w3.org/2003/05/soap-envelope"" xmlns:wsdl=""http://www.onvif.org/ver10/device/wsdl""><soap:Header/><soap:Body><wsdl:GetServices /></soap:Body></soap:Envelope>";
private const String GET_PROFILES_SOAP_TEMPLATE = @"<soap:Envelope xmlns:soap=""http://www.w3.org/2003/05/soap-envelope"" xmlns:wsdl=""http://www.onvif.org/ver10/media/wsdl"">
<soap:Header>
{0}
</soap:Header>
<soap:Body>
<wsdl:GetProfiles/>
</soap:Body>
</soap:Envelope>";
private const String GET_STREAMING_URI_SOAP_TEMPLATE = @"<soap:Envelope xmlns:soap=""http://www.w3.org/2003/05/soap-envelope"" xmlns:wsdl=""http://www.onvif.org/ver10/media/wsdl"" xmlns:sch=""http://www.onvif.org/ver10/schema"">
<soap:Header>
{0}
</soap:Header>
<soap:Body>
<wsdl:GetStreamUri>
<wsdl:StreamSetup>
<sch:Stream>RTP-Unicast</sch:Stream>
<sch:Transport>
<sch:Protocol>RTSP</sch:Protocol>
</sch:Transport>
</wsdl:StreamSetup>
<wsdl:ProfileToken>{1}</wsdl:ProfileToken>
</wsdl:GetStreamUri>
</soap:Body>
</soap:Envelope>";

// Regular expression pattern of environment variables that hold OPC UA DiscoveryURL
// The pattern is ONVIF_DEVICE_SERVICE_URL_ followed by 6 digit digest. e.g.
// ONVIF_DEVICE_SERVICE_URL_123456, ONVIF_DEVICE_SERVICE_URL_ABCDEF
private const string OnvifDeviceServiceUrlLabelPattern = "^ONVIF_DEVICE_SERVICE_URL_[A-F0-9]{6,6}$";
private const string OnvifDeviceUuidLabelPattern = "^ONVIF_DEVICE_UUID_[A-F0-9]{6,6}$";

private static string GetMediaUrl(String device_service_url)
{
var servicesResult = PostSoapRequest(
device_service_url,
String.Format("{0}/{1}", DEVICE_WSDL, "GetService"),
GET_SERVICE_SOAP
);
var document = new XPathDocument(new XmlTextReader(new StringReader(servicesResult)));
var navigator = document.CreateNavigator();
var xpath = String.Format("//*[local-name()='GetServicesResponse']/*[local-name()='Service' and *[local-name()='Namespace']/text() ='{0}']/*[local-name()='XAddr']/text()", MEDIA_WSDL);
var media_url = navigator.SelectSingleNode(xpath).ToString();
Console.WriteLine($"[Akri] ONVIF media url {media_url}");
return media_url;
}

private static string GetProfile(String media_url, UsernameToken usernameToken)
{
var soapSecurityHeader = usernameToken.ToXml();
var soapMessage = String.Format(GET_PROFILES_SOAP_TEMPLATE, soapSecurityHeader);
var servicesResult = PostSoapRequest(
media_url,
String.Format("{0}/{1}", MEDIA_WSDL, "GetProfiles"),
soapMessage
);
var document = new XPathDocument(new XmlTextReader(new StringReader(servicesResult)));
var navigator = document.CreateNavigator();
var xpath = String.Format("//*[local-name()='GetProfilesResponse']/*[local-name()='Profiles']/@token");
var profileNodesIterator = navigator.Select(xpath);
var profiles = (from XPathNavigator @group in profileNodesIterator select @group.Value).ToList();
profiles.Sort();
foreach (var p in profiles) {
Console.WriteLine($"[Akri] ONVIF profile list contains: {p}");
}
// randomly choose first profile
var profile = profiles.First();
Console.WriteLine($"[Akri] ONVIF profile list {profile}");
return profile;
}

private static string GetStreamingUri(String media_url, String profile_token, UsernameToken usernameToken)
{
var soapSecurityHeader = usernameToken.ToXml();
var soapMessage = String.Format(GET_STREAMING_URI_SOAP_TEMPLATE, soapSecurityHeader, profile_token);
var servicesResult = PostSoapRequest(
media_url,
String.Format("{0}/{1}", MEDIA_WSDL, "GetStreamUri"),
soapMessage
);
var document = new XPathDocument(new XmlTextReader(new StringReader(servicesResult)));
var navigator = document.CreateNavigator();
var xpath = String.Format("//*[local-name()='GetStreamUriResponse']/*[local-name()='MediaUri']/*[local-name()='Uri']/text()");
var profileNodesIterator = navigator.Select(xpath);
var streaming_uri_list = (from XPathNavigator @group in profileNodesIterator select @group.Value).ToList();
foreach (var u in streaming_uri_list) {
Console.WriteLine($"[Akri] ONVIF streaming uri list contains: {u}");
}
// randomly choose first profile
var streaming_uri = streaming_uri_list.First();
Console.WriteLine($"[Akri] ONVIF streaming uri {streaming_uri}");

const string rtspPrefix = "rtsp://";
if (streaming_uri.StartsWith(rtspPrefix)) {
if (!String.IsNullOrEmpty(usernameToken.Username)) {
var password = usernameToken.Password ?? "";
var credential_string = String.Format("{0}:{1}@", usernameToken.Username, password);
streaming_uri = streaming_uri.Substring(rtspPrefix.Length);
streaming_uri = String.Format("{0}{1}{2}", rtspPrefix, credential_string, streaming_uri);
}
}
return streaming_uri;
}

private static List<string> GetDeviceServiceUrls()
{
var values = new List<string>();
foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
{
if (Regex.IsMatch(de.Key.ToString(), OnvifDeviceServiceUrlLabelPattern))
{
values.Add(de.Value.ToString());
}
}
return values;
}

private static List<string> GetDeviceUuids()
{
var values = new List<string>();
foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
{
if (Regex.IsMatch(de.Key.ToString(), OnvifDeviceUuidLabelPattern))
{
values.Add(de.Value.ToString());
}
}
return values;
}

public static string GetRtspUrl()
{
var device_uuids = GetDeviceUuids();
var device_uuid = (device_uuids.Count != 0) ? device_uuids[0] : "";
Credential credential = null;
if (!string.IsNullOrEmpty(device_uuid))
{
var credentialDirectory = Environment.GetEnvironmentVariable("CREDENTIAL_DIRECTORY");
var credentialConfigMapDirectory = Environment.GetEnvironmentVariable("CREDENTIAL_CONFIGMAP_DIRECTORY");
var credentialStore = new CredentialStore(credentialDirectory, credentialConfigMapDirectory);
credential = credentialStore.Get(device_uuid);
}
var userNameToken = new UsernameToken(credential?.Username, credential?.Password);

// Get the first found Onvif device service url and use it
var device_service_urls = GetDeviceServiceUrls();
var device_service_url = (device_service_urls.Count != 0) ? device_service_urls[0] : "";
if (string.IsNullOrEmpty(device_service_url))
{
throw new ArgumentNullException("ONVIF_DEVICE_SERVICE_URL undefined");
}

var media_url = GetMediaUrl(device_service_url);
var profile = GetProfile(media_url, userNameToken);
var streaming_url = GetStreamingUri(media_url, profile, userNameToken);
return streaming_url;
}
}
}
Loading
Loading