Skip to content
Open
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
58 changes: 58 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Build and Push Docker Image

on:
push:
branches:
- main
pull_request:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

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

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
33 changes: 31 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ if (args) {
}

let proxies = {};
let proxyServers = [];
let servers = [];

for (let onvifConfig of config.onvif) {
let server = onvifServer.createServer(onvifConfig, logger);
Expand All @@ -95,12 +97,13 @@ if (args) {
server.startDiscovery();
if (args.debug)
server.enableDebugOutput();
servers.push(server);
logger.info(' Started!');
logger.info('');

if (!proxies[onvifConfig.target.hostname])
proxies[onvifConfig.target.hostname] = {}

if (onvifConfig.ports.rtsp && onvifConfig.target.ports.rtsp)
proxies[onvifConfig.target.hostname][onvifConfig.ports.rtsp] = onvifConfig.target.ports.rtsp;
if (onvifConfig.ports.snapshot && onvifConfig.target.ports.snapshot)
Expand All @@ -114,12 +117,38 @@ if (args) {
for (let destinationAddress in proxies) {
for (let sourcePort in proxies[destinationAddress]) {
logger.info(`Starting tcp proxy from port ${sourcePort} to ${destinationAddress}:${proxies[destinationAddress][sourcePort]} ...`);
tcpProxy.createProxy(sourcePort, destinationAddress, proxies[destinationAddress][sourcePort]);
const proxyServer = tcpProxy.createProxy(sourcePort, destinationAddress, proxies[destinationAddress][sourcePort]);
proxyServers.push(proxyServer);
logger.info(' Started!');
logger.info('');
}
}

// Graceful shutdown on SIGTERM/SIGINT (important for Docker)
const gracefulShutdown = async (signal) => {
logger.info(`Received ${signal}, shutting down gracefully...`);

// Shutdown all TCP proxies
logger.info('Closing TCP proxies...');
for (const proxy of proxyServers) {
try {
proxy.end();
} catch (err) {
logger.error('Error closing TCP proxy: ' + err.message);
}
}

// Shutdown all ONVIF servers
const shutdownPromises = servers.map(server => server.shutdown());
await Promise.all(shutdownPromises);

logger.info('All servers shut down successfully');
process.exit(0);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

} else {
logger.error('Please specifiy a config filename!');
return -1;
Expand Down
68 changes: 44 additions & 24 deletions src/config-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@ async function createConfig(hostname, username, password) {
hasNonce: true,
passwordType: 'PasswordDigest'
};

let client = await soap.createClientAsync('./wsdl/media_service.wsdl', options);
client.setEndpoint(`http://${hostname}/onvif/device_service`);
client.setSecurity(new soap.WSSecurity(username, password, securityOptions));

let hostport = 80;
if (hostname.indexOf(':') > -1) {
hostport = parseInt(hostname.substr(hostname.indexOf(':') + 1));
hostname = hostname.substr(0, hostname.indexOf(':'));
}
// Ensure client is cleaned up on error or completion
try {
client.setEndpoint(`http://${hostname}/onvif/device_service`);
client.setSecurity(new soap.WSSecurity(username, password, securityOptions));

let cameras = {};
let hostport = 80;
if (hostname.indexOf(':') > -1) {
hostport = parseInt(hostname.substr(hostname.indexOf(':') + 1));
hostname = hostname.substr(0, hostname.indexOf(':'));
}

let cameras = {};

try {
let profiles = await client.GetProfilesAsync({});
for (let profile of profiles[0].Profiles) {
let videoSource = profile.VideoSourceConfiguration.SourceToken;
Expand All @@ -53,18 +55,13 @@ async function createConfig(hostname, username, password) {
profile.snapshotUri = snapshotUri[0].MediaUri.Uri;
cameras[videoSource].push(profile);
}
} catch (err) {
if (err.root && err.root.Envelope && err.root.Envelope.Body && err.root.Envelope.Body.Fault && err.root.Envelope.Body.Fault.Reason && err.root.Envelope.Body.Fault.Reason.Text)
throw `Error: ${err.root.Envelope.Body.Fault.Reason.Text['$value']}`;
throw `Error: ${err.message}`;
}

let config = {
onvif: []
};
let config = {
onvif: []
};

let serverPort = 8081;
for (let camera in cameras) {
let serverPort = 8081;
for (let camera in cameras) {
let mainStream = cameras[camera][0];
let subStream = cameras[camera][cameras[camera].length > 1 ? 1 : 0];

Expand Down Expand Up @@ -117,16 +114,31 @@ async function createConfig(hostname, username, password) {
}
};

config.onvif.push(cameraConfig);
serverPort++;
}
config.onvif.push(cameraConfig);
serverPort++;
}

return config;
return config;
} catch (err) {
if (err.root && err.root.Envelope && err.root.Envelope.Body && err.root.Envelope.Body.Fault && err.root.Envelope.Body.Fault.Reason && err.root.Envelope.Body.Fault.Reason.Text)
throw `Error: ${err.root.Envelope.Body.Fault.Reason.Text['$value']}`;
throw `Error: ${err.message}`;
} finally {
// Clean up SOAP client to prevent memory leaks
if (client && client.httpClient) {
// Destroy any active HTTP agent connections
if (client.httpClient.agent && client.httpClient.agent.destroy) {
client.httpClient.agent.destroy();
}
}
}
}

exports.createConfig = async function(hostname, username, password) {

let config;
let originalGetUTCHours = null;

try {
config = await createConfig(hostname, username, password);
} catch (err) {
Expand All @@ -135,6 +147,9 @@ exports.createConfig = async function(hostname, username, password) {
console.log('Retrying...')

var utcHours = (new Date()).getUTCHours();

// Save original method before modifying prototype
originalGetUTCHours = Date.prototype.getUTCHours;
Date.prototype.getUTCHours = function() {
return utcHours + 1;
}
Expand All @@ -145,6 +160,11 @@ exports.createConfig = async function(hostname, username, password) {
console.log(err);
}
}
} finally {
// Restore original Date.prototype.getUTCHours if it was modified
if (originalGetUTCHours !== null) {
Date.prototype.getUTCHours = originalGetUTCHours;
}
}

return config;
Expand Down
Loading