Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13b4b08413 | |||
| e4c5f0412c | |||
| 24b17e3e0b | |||
| a9922ba91d | |||
| c83dbbc0cb | |||
| 9b9f705b4d | |||
| 42b875ce2b | |||
| ae891db72b | |||
| bd85e94b7d | |||
| 3b379ea3cc | |||
| af2f0624c8 | |||
| d337cf5526 | |||
| 52352dacd4 | |||
| 64ce3192a4 | |||
| 41e8093594 | |||
| 16f697965d | |||
| b1c33164e3 | |||
| ea382eb9dc | |||
| a6fda445f3 |
@@ -1,120 +0,0 @@
|
|||||||
# Example GitHub Actions workflow for camera integration tests
|
|
||||||
# Save as .github/workflows/camera-tests.yml
|
|
||||||
|
|
||||||
name: Camera Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Run on manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
camera_endpoint:
|
|
||||||
description: 'Camera ONVIF endpoint'
|
|
||||||
required: true
|
|
||||||
default: 'http://192.168.1.201/onvif/device_service'
|
|
||||||
camera_username:
|
|
||||||
description: 'Camera username'
|
|
||||||
required: true
|
|
||||||
default: 'service'
|
|
||||||
|
|
||||||
# Or run on schedule (daily at 2 AM)
|
|
||||||
schedule:
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-bosch-flexidome:
|
|
||||||
name: Test Bosch FLEXIDOME
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# Only run if secrets are configured
|
|
||||||
if: ${{ secrets.ONVIF_TEST_PASSWORD != '' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.21'
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
- name: Run Bosch FLEXIDOME tests
|
|
||||||
env:
|
|
||||||
ONVIF_TEST_ENDPOINT: ${{ github.event.inputs.camera_endpoint || secrets.ONVIF_TEST_ENDPOINT }}
|
|
||||||
ONVIF_TEST_USERNAME: ${{ github.event.inputs.camera_username || secrets.ONVIF_TEST_USERNAME }}
|
|
||||||
ONVIF_TEST_PASSWORD: ${{ secrets.ONVIF_TEST_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
echo "Testing camera at: $ONVIF_TEST_ENDPOINT"
|
|
||||||
go test -v -run TestBoschFLEXIDOMEIndoor5100iIR -timeout 5m
|
|
||||||
|
|
||||||
- name: Run benchmarks
|
|
||||||
if: success()
|
|
||||||
env:
|
|
||||||
ONVIF_TEST_ENDPOINT: ${{ github.event.inputs.camera_endpoint || secrets.ONVIF_TEST_ENDPOINT }}
|
|
||||||
ONVIF_TEST_USERNAME: ${{ github.event.inputs.camera_username || secrets.ONVIF_TEST_USERNAME }}
|
|
||||||
ONVIF_TEST_PASSWORD: ${{ secrets.ONVIF_TEST_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
go test -bench=BenchmarkBoschFLEXIDOMEIndoor5100iIR -benchmem -run=^$ | tee benchmark.txt
|
|
||||||
|
|
||||||
- name: Upload benchmark results
|
|
||||||
if: success()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: benchmark-results
|
|
||||||
path: benchmark.txt
|
|
||||||
|
|
||||||
- name: Generate test coverage
|
|
||||||
if: success()
|
|
||||||
env:
|
|
||||||
ONVIF_TEST_ENDPOINT: ${{ github.event.inputs.camera_endpoint || secrets.ONVIF_TEST_ENDPOINT }}
|
|
||||||
ONVIF_TEST_USERNAME: ${{ github.event.inputs.camera_username || secrets.ONVIF_TEST_USERNAME }}
|
|
||||||
ONVIF_TEST_PASSWORD: ${{ secrets.ONVIF_TEST_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
go test -coverprofile=coverage.out -run TestBoschFLEXIDOMEIndoor5100iIR
|
|
||||||
go tool cover -html=coverage.out -o coverage.html
|
|
||||||
|
|
||||||
- name: Upload coverage report
|
|
||||||
if: success()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: coverage-report
|
|
||||||
path: coverage.html
|
|
||||||
|
|
||||||
- name: Comment test results
|
|
||||||
if: always() && github.event_name == 'workflow_dispatch'
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const outcome = '${{ job.status }}' === 'success' ? '✅ PASSED' : '❌ FAILED';
|
|
||||||
console.log(`Camera integration tests: ${outcome}`);
|
|
||||||
|
|
||||||
# Configuration Instructions:
|
|
||||||
#
|
|
||||||
# 1. Add secrets to your GitHub repository:
|
|
||||||
# - Go to Settings > Secrets and variables > Actions
|
|
||||||
# - Add the following secrets:
|
|
||||||
# * ONVIF_TEST_ENDPOINT (camera URL)
|
|
||||||
# * ONVIF_TEST_USERNAME (camera username)
|
|
||||||
# * ONVIF_TEST_PASSWORD (camera password)
|
|
||||||
#
|
|
||||||
# 2. Ensure your GitHub Actions runner can reach the camera:
|
|
||||||
# - Use self-hosted runner on same network as camera
|
|
||||||
# - Or use VPN/tunnel to access camera from GitHub-hosted runner
|
|
||||||
#
|
|
||||||
# 3. Run manually:
|
|
||||||
# - Go to Actions tab
|
|
||||||
# - Select "Camera Integration Tests"
|
|
||||||
# - Click "Run workflow"
|
|
||||||
# - Optionally override endpoint/username
|
|
||||||
@@ -96,7 +96,7 @@ Help us maintain compatibility information:
|
|||||||
### Clone and Build
|
### Clone and Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/0x524A/go-onvif.git
|
git clone https://github.com/0x524a/onvif-go.git
|
||||||
cd go-onvif
|
cd go-onvif
|
||||||
go build ./...
|
go build ./...
|
||||||
```
|
```
|
||||||
@@ -262,9 +262,9 @@ go-onvif/
|
|||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
- 💬 [GitHub Discussions](https://github.com/0x524A/go-onvif/discussions) - Ask questions
|
- 💬 [GitHub Discussions](https://github.com/0x524a/onvif-go/discussions) - Ask questions
|
||||||
- 🐛 [GitHub Issues](https://github.com/0x524A/go-onvif/issues) - Report bugs
|
- 🐛 [GitHub Issues](https://github.com/0x524a/onvif-go/issues) - Report bugs
|
||||||
- 📖 [Documentation](https://pkg.go.dev/github.com/0x524A/go-onvif) - Read the docs
|
- 📖 [Documentation](https://pkg.go.dev/github.com/0x524a/onvif-go) - Read the docs
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/0x524A/go-onvif"
|
import "github.com/0x524a/onvif-go"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Your code here
|
// Your code here
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Discussions
|
- name: 💬 Discussions
|
||||||
url: https://github.com/0x524A/go-onvif/discussions
|
url: https://github.com/0x524a/onvif-go/discussions
|
||||||
about: Ask questions and discuss ideas with the community
|
about: Ask questions and discuss ideas with the community
|
||||||
- name: 📖 Documentation
|
- name: 📖 Documentation
|
||||||
url: https://pkg.go.dev/github.com/0x524A/go-onvif
|
url: https://pkg.go.dev/github.com/0x524a/onvif-go
|
||||||
about: Read the API documentation
|
about: Read the API documentation
|
||||||
- name: 📚 Examples
|
- name: 📚 Examples
|
||||||
url: https://github.com/0x524A/go-onvif/tree/main/examples
|
url: https://github.com/0x524a/onvif-go/tree/main/examples
|
||||||
about: Browse code examples
|
about: Browse code examples
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Release Binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Linux
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: 7
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
- goos: windows
|
||||||
|
goarch: amd64
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.VERSION }}
|
||||||
|
LDFLAGS="-s -w -X main.Version=${VERSION} -X main.Commit=${{ steps.version.outputs.SHORT_SHA }}"
|
||||||
|
|
||||||
|
# Set file extension for Windows
|
||||||
|
EXT=""
|
||||||
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
EXT=".exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build all CLI tools
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
echo "Building onvif-cli..."
|
||||||
|
go build -ldflags="${LDFLAGS}" -o "dist/onvif-cli-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-cli
|
||||||
|
|
||||||
|
echo "Building onvif-quick..."
|
||||||
|
go build -ldflags="${LDFLAGS}" -o "dist/onvif-quick-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-quick
|
||||||
|
|
||||||
|
echo "Building onvif-server..."
|
||||||
|
go build -ldflags="${LDFLAGS}" -o "dist/onvif-server-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-server
|
||||||
|
|
||||||
|
echo "Building onvif-diagnostics..."
|
||||||
|
go build -ldflags="${LDFLAGS}" -o "dist/onvif-diagnostics-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
|
- name: Create archive
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.VERSION }}
|
||||||
|
PLATFORM="${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
ARCHIVE_NAME="go-onvif-${VERSION}-${PLATFORM}"
|
||||||
|
|
||||||
|
mkdir -p releases
|
||||||
|
|
||||||
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
|
# Create ZIP for Windows
|
||||||
|
cd dist
|
||||||
|
zip -j "../releases/${ARCHIVE_NAME}.zip" *-${{ matrix.goos }}-${{ matrix.goarch }}.exe ../README.md ../LICENSE
|
||||||
|
cd ..
|
||||||
|
else
|
||||||
|
# Create tar.gz for Unix-like systems
|
||||||
|
cd dist
|
||||||
|
tar czf "../releases/${ARCHIVE_NAME}.tar.gz" *-${{ matrix.goos }}-${{ matrix.goarch }} -C .. README.md LICENSE
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd releases
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum * > checksums-${{ matrix.goos }}-${{ matrix.goarch }}.txt
|
||||||
|
else
|
||||||
|
shasum -a 256 * > checksums-${{ matrix.goos }}-${{ matrix.goarch }}.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: releases/*
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: all-releases
|
||||||
|
pattern: release-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Generate combined checksums
|
||||||
|
run: |
|
||||||
|
cd all-releases
|
||||||
|
# Combine all checksum files
|
||||||
|
cat checksums-*.txt > checksums.txt
|
||||||
|
# Remove individual checksum files
|
||||||
|
rm checksums-*.txt
|
||||||
|
|
||||||
|
- name: Get version and changelog
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Generate changelog from commits since last tag
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD >> $GITHUB_OUTPUT
|
||||||
|
echo "" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: all-releases/*
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
|
||||||
|
generate_release_notes: true
|
||||||
|
body: |
|
||||||
|
## Release ${{ steps.version.outputs.VERSION }}
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Download the appropriate binary for your platform below.
|
||||||
|
|
||||||
|
#### Linux/macOS
|
||||||
|
```bash
|
||||||
|
# Download and extract
|
||||||
|
wget https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.VERSION }}/go-onvif-${{ steps.version.outputs.VERSION }}-linux-amd64.tar.gz
|
||||||
|
tar xzf go-onvif-${{ steps.version.outputs.VERSION }}-linux-amd64.tar.gz
|
||||||
|
|
||||||
|
# Make executable and move to PATH
|
||||||
|
chmod +x onvif-cli-linux-amd64
|
||||||
|
sudo mv onvif-cli-linux-amd64 /usr/local/bin/onvif-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
Download the `.zip` file for your architecture and extract it.
|
||||||
|
|
||||||
|
#### Go Library
|
||||||
|
```bash
|
||||||
|
go get github.com/${{ github.repository }}@${{ steps.version.outputs.VERSION }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
|
||||||
|
SHA256 checksums are available in `checksums.txt`
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
${{ steps.version.outputs.CHANGELOG }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Build and Push Docker Image
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:latest
|
||||||
|
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
+8
-5
@@ -26,13 +26,16 @@ go.work
|
|||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Binaries
|
# Binaries (in root, bin, or dist directories)
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
onvif-diagnostics
|
releases/
|
||||||
onvif-server
|
/onvif-diagnostics
|
||||||
onvif-server-example
|
/onvif-server
|
||||||
generate-tests
|
/onvif-server-example
|
||||||
|
/generate-tests
|
||||||
|
/onvif-cli
|
||||||
|
/onvif-quick
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
|||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
# Building and Releasing go-onvif
|
||||||
|
|
||||||
|
This document describes how to build binaries for multiple platforms and create releases.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build for Your Current Platform
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds all CLI tools for your current OS/architecture in the `bin/` directory.
|
||||||
|
|
||||||
|
### Build for All Platforms
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-all
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates binaries for:
|
||||||
|
- **Linux**: amd64, arm64, arm (32-bit)
|
||||||
|
- **Windows**: amd64, arm64
|
||||||
|
- **macOS**: amd64 (Intel), arm64 (Apple Silicon)
|
||||||
|
|
||||||
|
Binaries are output to `bin/` directory.
|
||||||
|
|
||||||
|
### Create Release Archives
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release
|
||||||
|
```
|
||||||
|
|
||||||
|
This:
|
||||||
|
1. Builds for all platforms
|
||||||
|
2. Creates `.tar.gz` archives (Linux/macOS) and `.zip` files (Windows)
|
||||||
|
3. Generates SHA256 checksums
|
||||||
|
4. Places everything in `releases/` directory
|
||||||
|
|
||||||
|
## Manual Building
|
||||||
|
|
||||||
|
### Using the Build Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with automatic version detection
|
||||||
|
./build-release.sh
|
||||||
|
|
||||||
|
# Build with specific version
|
||||||
|
./build-release.sh v1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Go Directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set platform and architecture
|
||||||
|
export GOOS=linux
|
||||||
|
export GOARCH=amd64
|
||||||
|
|
||||||
|
# Build a specific tool
|
||||||
|
go build -o bin/onvif-cli-linux-amd64 ./cmd/onvif-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
| OS | Architecture | Binary Suffix | Notes |
|
||||||
|
|---------|-------------|------------------------|----------------------------|
|
||||||
|
| Linux | amd64 | `linux-amd64` | 64-bit Intel/AMD |
|
||||||
|
| Linux | arm64 | `linux-arm64` | 64-bit ARM (Raspberry Pi 4)|
|
||||||
|
| Linux | arm | `linux-arm` | 32-bit ARM (Raspberry Pi 3)|
|
||||||
|
| Windows | amd64 | `windows-amd64.exe` | 64-bit Windows |
|
||||||
|
| Windows | arm64 | `windows-arm64.exe` | ARM Windows (Surface Pro X)|
|
||||||
|
| macOS | amd64 | `darwin-amd64` | Intel Macs |
|
||||||
|
| macOS | arm64 | `darwin-arm64` | Apple Silicon (M1/M2/M3) |
|
||||||
|
|
||||||
|
## CLI Tools
|
||||||
|
|
||||||
|
The following binaries are built:
|
||||||
|
|
||||||
|
1. **onvif-cli** - Comprehensive ONVIF client with full feature set
|
||||||
|
2. **onvif-quick** - Quick tool for common operations
|
||||||
|
3. **onvif-server** - ONVIF mock server for testing
|
||||||
|
4. **onvif-diagnostics** - Diagnostic and debugging tools
|
||||||
|
|
||||||
|
## Automated Releases via GitHub Actions
|
||||||
|
|
||||||
|
Releases are automatically created when you push a tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create and push a new version tag
|
||||||
|
git tag -a v1.0.1 -m "Release version 1.0.1"
|
||||||
|
git push origin v1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The GitHub Actions workflow will:
|
||||||
|
1. Build binaries for all platforms
|
||||||
|
2. Create release archives
|
||||||
|
3. Generate checksums
|
||||||
|
4. Create a GitHub release with all artifacts
|
||||||
|
5. Build and push Docker images (multi-arch)
|
||||||
|
|
||||||
|
### Release Workflow Features
|
||||||
|
|
||||||
|
- ✅ Builds for 7 platform/architecture combinations
|
||||||
|
- ✅ Creates compressed archives (`.tar.gz` and `.zip`)
|
||||||
|
- ✅ Generates SHA256 checksums for verification
|
||||||
|
- ✅ Auto-generates release notes from commits
|
||||||
|
- ✅ Supports pre-releases (tags with `-rc`, `-beta`, `-alpha`)
|
||||||
|
- ✅ Builds multi-architecture Docker images
|
||||||
|
- ✅ Pushes to GitHub Container Registry
|
||||||
|
|
||||||
|
## Docker Images
|
||||||
|
|
||||||
|
Docker images are automatically built for:
|
||||||
|
- `linux/amd64`
|
||||||
|
- `linux/arm64`
|
||||||
|
- `linux/arm/v7`
|
||||||
|
|
||||||
|
Available at:
|
||||||
|
```
|
||||||
|
ghcr.io/0x524a/onvif-go:latest
|
||||||
|
ghcr.io/0x524a/onvif-go:v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual GitHub Release
|
||||||
|
|
||||||
|
If you prefer to create releases manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build release archives
|
||||||
|
make release
|
||||||
|
|
||||||
|
# Create GitHub release using gh CLI
|
||||||
|
gh release create v1.0.1 releases/* \
|
||||||
|
--title "Release v1.0.1" \
|
||||||
|
--notes "Release notes here"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Numbering
|
||||||
|
|
||||||
|
Follow [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
|
- `v1.0.0` - Major release (breaking changes)
|
||||||
|
- `v1.1.0` - Minor release (new features, backward compatible)
|
||||||
|
- `v1.1.1` - Patch release (bug fixes)
|
||||||
|
- `v1.0.0-rc1` - Release candidate
|
||||||
|
- `v1.0.0-beta1` - Beta release
|
||||||
|
- `v1.0.0-alpha1` - Alpha release
|
||||||
|
|
||||||
|
## Build Flags
|
||||||
|
|
||||||
|
The build process uses the following flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-ldflags="-s -w -X main.Version=<version> -X main.Commit=<sha>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-s` - Omit symbol table (smaller binary)
|
||||||
|
- `-w` - Omit DWARF debug info (smaller binary)
|
||||||
|
- `-X main.Version` - Inject version string
|
||||||
|
- `-X main.Commit` - Inject git commit SHA
|
||||||
|
|
||||||
|
## Size Optimization
|
||||||
|
|
||||||
|
Binaries are built with `CGO_ENABLED=0` and stripped flags, resulting in:
|
||||||
|
- Smaller binary sizes
|
||||||
|
- No external dependencies
|
||||||
|
- Portable across systems
|
||||||
|
|
||||||
|
Typical sizes:
|
||||||
|
- onvif-cli: ~10-15 MB
|
||||||
|
- onvif-quick: ~8-12 MB
|
||||||
|
- onvif-server: ~10-14 MB
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Fails for Specific Platform
|
||||||
|
|
||||||
|
Some platforms may not be supported by all dependencies. Check:
|
||||||
|
```bash
|
||||||
|
go tool dist list # List all supported platforms
|
||||||
|
```
|
||||||
|
|
||||||
|
### Large Binary Sizes
|
||||||
|
|
||||||
|
Ensure you're using the build flags:
|
||||||
|
```bash
|
||||||
|
go build -ldflags="-s -w" -o binary ./cmd/tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
Once built, binaries can be distributed via:
|
||||||
|
|
||||||
|
1. **GitHub Releases** (automatic)
|
||||||
|
2. **Package managers** (homebrew, apt, etc.)
|
||||||
|
3. **Container registries** (Docker Hub, GHCR)
|
||||||
|
4. **Direct download** from your server
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Users can verify downloads using checksums:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download binary and checksum
|
||||||
|
wget https://github.com/0x524a/onvif-go/releases/download/v1.0.0/onvif-go-v1.0.0-linux-amd64.tar.gz
|
||||||
|
wget https://github.com/0x524a/onvif-go/releases/download/v1.0.0/checksums.txt
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
sha256sum -c checksums.txt --ignore-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After building:
|
||||||
|
1. Test binaries on target platforms
|
||||||
|
2. Update CHANGELOG.md with release notes
|
||||||
|
3. Create GitHub release
|
||||||
|
4. Announce on relevant channels
|
||||||
|
5. Update documentation with new features
|
||||||
@@ -1,706 +0,0 @@
|
|||||||
# ONVIF Camera Analysis Report
|
|
||||||
|
|
||||||
Generated: November 7, 2025
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
Analysis of 5 ONVIF-compliant cameras from 3 manufacturers (REOLINK, AXIS, Bosch) reveals diverse implementations and capabilities. All cameras successfully responded to ONVIF commands with varying feature sets.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Camera Inventory
|
|
||||||
|
|
||||||
### 1. REOLINK E1 Zoom
|
|
||||||
- **Firmware**: v3.1.0.2649_23083101
|
|
||||||
- **Serial**: 192168261
|
|
||||||
- **IP**: 192.168.2.61:8000
|
|
||||||
- **Type**: PTZ Indoor Camera
|
|
||||||
- **Key Features**: PTZ support, dual stream, basic imaging
|
|
||||||
|
|
||||||
### 2. AXIS Q3819-PVE
|
|
||||||
- **Firmware**: 10.12.153
|
|
||||||
- **Serial**: B8A44F9DC7ED
|
|
||||||
- **IP**: 192.168.2.190
|
|
||||||
- **Type**: Panoramic Fixed Dome
|
|
||||||
- **Key Features**: Ultra-wide 8192x1728 resolution, analytics, advanced imaging
|
|
||||||
|
|
||||||
### 3. AXIS P3818-PVE
|
|
||||||
- **Firmware**: 11.9.60
|
|
||||||
- **Serial**: B8A44FA04F26
|
|
||||||
- **IP**: 192.168.2.82
|
|
||||||
- **Type**: Panoramic Fixed Dome
|
|
||||||
- **Key Features**: 5120x2560 resolution, analytics, dual encoding (H264/JPEG)
|
|
||||||
|
|
||||||
### 4. Bosch FLEXIDOME panoramic 5100i
|
|
||||||
- **Firmware**: 9.00.0210
|
|
||||||
- **Serial**: 404705923918060213
|
|
||||||
- **IP**: 192.168.2.24
|
|
||||||
- **Type**: 360° Panoramic Dome
|
|
||||||
- **Key Features**: 16 profiles, dewarping, circular image (2112x2112)
|
|
||||||
|
|
||||||
### 5. Bosch FLEXIDOME IP starlight 8000i
|
|
||||||
- **Firmware**: 7.70.0126
|
|
||||||
- **Serial**: 044518807925140011
|
|
||||||
- **IP**: 192.168.2.200
|
|
||||||
- **Type**: Fixed Dome with Low-Light Performance
|
|
||||||
- **Key Features**: Starlight imaging, I/O connectors, relay output
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparative Analysis
|
|
||||||
|
|
||||||
### Resolution Capabilities
|
|
||||||
|
|
||||||
| Camera | Max Resolution | Aspect Ratio | Primary Use Case |
|
|
||||||
|--------|---------------|--------------|------------------|
|
|
||||||
| REOLINK E1 Zoom | 2048x1536 | 4:3 | Standard surveillance |
|
|
||||||
| AXIS Q3819-PVE | 8192x1728 | ~4.7:1 | 180° panoramic |
|
|
||||||
| AXIS P3818-PVE | 5120x2560 | 2:1 | 180° panoramic |
|
|
||||||
| Bosch panoramic 5100i | 2112x2112 | 1:1 | 360° fisheye |
|
|
||||||
| Bosch starlight 8000i | 1536x864 | 16:9 | Low-light environments |
|
|
||||||
|
|
||||||
### Profile Count
|
|
||||||
|
|
||||||
| Camera | Total Profiles | Video Profiles | Notes |
|
|
||||||
|--------|----------------|----------------|-------|
|
|
||||||
| REOLINK E1 Zoom | 2 | 2 | MainStream + SubStream |
|
|
||||||
| AXIS Q3819-PVE | 2 | 2 | H264 + JPEG |
|
|
||||||
| AXIS P3818-PVE | 2 | 2 | H264 + JPEG |
|
|
||||||
| Bosch panoramic 5100i | 16 | 9 valid | Includes metadata/audio profiles |
|
|
||||||
| Bosch starlight 8000i | 3 | 3 | 2x H264 + 1x JPEG |
|
|
||||||
|
|
||||||
### ONVIF Service Support
|
|
||||||
|
|
||||||
| Service | REOLINK | AXIS Q3819 | AXIS P3818 | Bosch Panoramic | Bosch Starlight |
|
|
||||||
|---------|---------|------------|------------|-----------------|-----------------|
|
|
||||||
| Device | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| Media | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| Imaging | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| Events | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| Analytics | ✗ | ✓ | ✓ | ✓ | ✗ |
|
|
||||||
| PTZ | ✓ | ✗ | ✗ | ✓ | ✗ |
|
|
||||||
|
|
||||||
### Video Encoding
|
|
||||||
|
|
||||||
| Camera | H264 | JPEG | MPEG4 | Notes |
|
|
||||||
|--------|------|------|-------|-------|
|
|
||||||
| REOLINK | ✓ | ✗ | ✗ | H264 only |
|
|
||||||
| AXIS Q3819 | ✓ | ✓ | ✗ | Dual encoding |
|
|
||||||
| AXIS P3818 | ✓ | ✓ | ✗ | Dual encoding |
|
|
||||||
| Bosch Panoramic | ✓ | ✗ | ✗ | H264 only |
|
|
||||||
| Bosch Starlight | ✓ | ✓ | ✗ | Dual encoding |
|
|
||||||
|
|
||||||
### Network Capabilities
|
|
||||||
|
|
||||||
| Feature | REOLINK | AXIS Q3819 | AXIS P3818 | Bosch Panoramic | Bosch Starlight |
|
|
||||||
|---------|---------|------------|------------|-----------------|-----------------|
|
|
||||||
| RTP Multicast | ✗ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| RTP/TCP | ✓ | ✓ | ✓ | ✗ | ✗ |
|
|
||||||
| RTP/RTSP/TCP | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
| IPv6 Support | ✗ | ✓ | ✓ | ✗ | ✗ |
|
|
||||||
| TLS 1.2 | ✗ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
|
|
||||||
### Imaging Features
|
|
||||||
|
|
||||||
| Feature | REOLINK | AXIS Q3819 | AXIS P3818 | Bosch Panoramic | Bosch Starlight |
|
|
||||||
|---------|---------|------------|------------|-----------------|-----------------|
|
|
||||||
| Brightness Control | ✓ (128) | ✓ (50) | ✓ (50) | ✓ (127) | ✓ (128) |
|
|
||||||
| Saturation Control | ✓ (128) | ✓ (50) | ✓ (50) | ✓ (127) | ✓ (128) |
|
|
||||||
| Contrast Control | ✓ (128) | ✓ (50) | ✓ (50) | ✓ (127) | ✓ (128) |
|
|
||||||
| Sharpness Control | ✓ (128) | ✓ (50) | ✓ (50) | ✗ | ✗ |
|
|
||||||
| IrCutFilter | AUTO | AUTO | AUTO | ✗ | ✗ |
|
|
||||||
| WDR | ✗ | ON | ON | ✗ | ✗ |
|
|
||||||
| WhiteBalance | ✗ | AUTO | AUTO | ✗ | ✗ |
|
|
||||||
| Exposure Control | ✗ | AUTO | AUTO | ✗ | ✗ |
|
|
||||||
|
|
||||||
### I/O and Security
|
|
||||||
|
|
||||||
| Feature | REOLINK | AXIS Q3819 | AXIS P3818 | Bosch Panoramic | Bosch Starlight |
|
|
||||||
|---------|---------|------------|------------|-----------------|-----------------|
|
|
||||||
| Input Connectors | 0 | 2 | 2 | 0 | 2 |
|
|
||||||
| Relay Outputs | 0 | 0 | 0 | 0 | 1 |
|
|
||||||
| IP Filter | ✗ | ✓ | ✓ | ✗ | ✗ |
|
|
||||||
| TLS 1.1 | ✗ | ✓ | ✓ | ✗ | ✓ |
|
|
||||||
| TLS 1.2 | ✗ | ✓ | ✓ | ✓ | ✓ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Manufacturer-Specific Findings
|
|
||||||
|
|
||||||
### REOLINK
|
|
||||||
- **Strengths**:
|
|
||||||
- Simple, straightforward ONVIF implementation
|
|
||||||
- PTZ support with status reporting
|
|
||||||
- Good value camera with basic features
|
|
||||||
- **Limitations**:
|
|
||||||
- Limited imaging controls (no WDR, exposure, focus)
|
|
||||||
- Only H264 encoding (no JPEG profile)
|
|
||||||
- No analytics support
|
|
||||||
- Lower security features (no TLS)
|
|
||||||
- **RTSP Pattern**: `rtsp://IP:554/` (main), `rtsp://IP:554/h264Preview_01_sub` (sub)
|
|
||||||
- **Snapshot Pattern**: `http://IP:80/cgi-bin/api.cgi?cmd=onvifSnapPic&channel=0`
|
|
||||||
|
|
||||||
### AXIS
|
|
||||||
- **Strengths**:
|
|
||||||
- Excellent ONVIF compliance and feature richness
|
|
||||||
- Ultra-high resolution panoramic cameras
|
|
||||||
- Advanced imaging with WDR, exposure control, white balance
|
|
||||||
- Strong security (TLS 1.1/1.2, IP filtering, access policy)
|
|
||||||
- Analytics and rule-based event support
|
|
||||||
- **Consistent Implementation**:
|
|
||||||
- Both cameras share similar ONVIF structure
|
|
||||||
- Dual H264/JPEG encoding profiles
|
|
||||||
- Same URL patterns and capabilities
|
|
||||||
- **RTSP Pattern**: `rtsp://IP/onvif-media/media.amp?profile=X&sessiontimeout=60&streamtype=unicast`
|
|
||||||
- **Snapshot Pattern**: `http://IP/onvif-cgi/jpg/image.cgi?resolution=WxH&compression=30`
|
|
||||||
- **Notable**: Q3819 has wider aspect ratio (8192x1728 vs 5120x2560)
|
|
||||||
|
|
||||||
### Bosch
|
|
||||||
- **Strengths**:
|
|
||||||
- Specialized cameras with unique features
|
|
||||||
- Panoramic 5100i has comprehensive dewarping profiles
|
|
||||||
- Starlight 8000i optimized for low-light
|
|
||||||
- Good I/O options (starlight model has relay output)
|
|
||||||
- **Quirks**:
|
|
||||||
- Panoramic model has 16 profiles (many without video encoders)
|
|
||||||
- Some profiles return "IncompleteConfiguration" errors
|
|
||||||
- Less standardized RTSP URLs (tunnel-based)
|
|
||||||
- **RTSP Pattern**: `rtsp://IP/rtsp_tunnel?p=X&line=Y&inst=Z` (various parameters)
|
|
||||||
- **Snapshot Pattern**: `http://IP/snap.jpg?JpegCam=X`
|
|
||||||
- **Notable**:
|
|
||||||
- Panoramic uses circular (2112x2112) and dewarped (3072x1728) views
|
|
||||||
- 3 profiles failed GetStreamURI with incomplete configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Metrics
|
|
||||||
|
|
||||||
### Response Times (Average)
|
|
||||||
|
|
||||||
| Operation | REOLINK | AXIS Q3819 | AXIS P3818 | Bosch Panoramic | Bosch Starlight |
|
|
||||||
|-----------|---------|------------|------------|-----------------|-----------------|
|
|
||||||
| DeviceInfo | 117.7ms | 5.0ms | 4.9ms | 8.5ms | 7.9ms |
|
|
||||||
| Capabilities | 85.6ms | 72.7ms | 69.3ms | 21.9ms | 27.1ms |
|
|
||||||
| GetProfiles | 832.1ms | 70.9ms | 8.0ms | 706.2ms | 258.3ms |
|
|
||||||
| GetStreamURI | ~129ms avg | ~20ms avg | ~4ms avg | ~11ms avg | ~10ms avg |
|
|
||||||
| GetSnapshot | ~170ms avg | ~20ms avg | ~4ms avg | ~11ms avg | ~6ms avg |
|
|
||||||
| Imaging | 111.8ms | 55.8ms | 67.2ms | 57.3ms | 14.8ms |
|
|
||||||
|
|
||||||
**Key Observations**:
|
|
||||||
- AXIS cameras have fastest response times overall
|
|
||||||
- REOLINK has higher latency (likely due to port 8000, may be proxy/gateway)
|
|
||||||
- Bosch cameras have moderate, consistent response times
|
|
||||||
- GetProfiles is slowest operation for most cameras
|
|
||||||
|
|
||||||
### Error Analysis
|
|
||||||
|
|
||||||
| Camera | Total Errors | Error Types |
|
|
||||||
|--------|--------------|-------------|
|
|
||||||
| REOLINK E1 Zoom | 0 | None |
|
|
||||||
| AXIS Q3819-PVE | 0 | None |
|
|
||||||
| AXIS P3818-PVE | 0 | None |
|
|
||||||
| Bosch panoramic 5100i | 3 | GetStreamURI: IncompleteConfiguration (profiles 9,10,11) |
|
|
||||||
| Bosch starlight 8000i | 0 | None |
|
|
||||||
|
|
||||||
**Bosch Panoramic Errors**: Profiles 9, 10, 11 have no VideoEncoderConfiguration, causing legitimate failures. These appear to be metadata-only or incomplete profiles.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stream URI Patterns
|
|
||||||
|
|
||||||
### REOLINK Pattern
|
|
||||||
```
|
|
||||||
rtsp://192.168.2.61:554/ # MainStream
|
|
||||||
rtsp://192.168.2.61:554/h264Preview_01_sub # SubStream
|
|
||||||
```
|
|
||||||
|
|
||||||
### AXIS Pattern
|
|
||||||
```
|
|
||||||
rtsp://IP/onvif-media/media.amp?profile=profile_1_h264&sessiontimeout=60&streamtype=unicast
|
|
||||||
rtsp://IP/onvif-media/media.amp?profile=profile_1_jpeg&sessiontimeout=60&streamtype=unicast
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bosch Patterns
|
|
||||||
|
|
||||||
**Indoor 5100i IR** (from previous report):
|
|
||||||
```
|
|
||||||
rtsp://IP/rtsp_tunnel?p=0&line=1&inst=1&vcd=2
|
|
||||||
```
|
|
||||||
|
|
||||||
**Panoramic 5100i**:
|
|
||||||
```
|
|
||||||
rtsp://192.168.2.24/rtsp_tunnel?p=0&line=3&inst=4 # E_PTZ view
|
|
||||||
rtsp://192.168.2.24/rtsp_tunnel?p=1&line=2&inst=1 # Dewarped view
|
|
||||||
rtsp://192.168.2.24/rtsp_tunnel?p=2&line=1&inst=4 # Full circle
|
|
||||||
rtsp://192.168.2.24/rtsp_tunnel?von=0&aon=1&aud=1 # Audio only
|
|
||||||
rtsp://192.168.2.24/rtsp_tunnel?von=0&vcd=2&line=1 # Metadata
|
|
||||||
```
|
|
||||||
|
|
||||||
**Starlight 8000i**:
|
|
||||||
```
|
|
||||||
rtsp://192.168.2.200/rtsp_tunnel?p=0&h26x=4&vcd=2
|
|
||||||
rtsp://192.168.2.200/rtsp_tunnel?p=1&inst=2&h26x=4
|
|
||||||
rtsp://192.168.2.200/rtsp_tunnel?h26x=0 # JPEG
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameter Meanings**:
|
|
||||||
- `p`: Profile index
|
|
||||||
- `line`: Video line/source (1=full, 2=dewarped, 3=ePTZ)
|
|
||||||
- `inst`: Instance number
|
|
||||||
- `vcd`: Video codec (2=metadata)
|
|
||||||
- `h26x`: H.26x codec (0=JPEG, 4=H264)
|
|
||||||
- `von`: Video on/off
|
|
||||||
- `aon`: Audio on/off
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PTZ Capabilities
|
|
||||||
|
|
||||||
### REOLINK E1 Zoom (PTZ Enabled)
|
|
||||||
- **PTZ Service**: http://192.168.2.61:8000/onvif/ptz_service
|
|
||||||
- **Status**: Both profiles report IDLE for PanTilt and Zoom
|
|
||||||
- **Presets**: 0 configured
|
|
||||||
- **Configuration**: PTZ config present but with empty position spaces
|
|
||||||
- **Notes**: PTZ capability exists but requires further testing for movement commands
|
|
||||||
|
|
||||||
### Bosch Panoramic 5100i (ePTZ)
|
|
||||||
- **PTZ Service**: http://192.168.2.24/onvif/ptz_service
|
|
||||||
- **Type**: Electronic PTZ (digital zoom/pan on panoramic image)
|
|
||||||
- **Profile**: Dedicated ePTZ profile (token "0", 1920x1080)
|
|
||||||
- **Notes**: Digital PTZ on dewarped 360° image, not mechanical movement
|
|
||||||
|
|
||||||
### Other Cameras
|
|
||||||
- AXIS Q3819-PVE, P3818-PVE, Bosch starlight 8000i: No PTZ support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Snapshot URI Patterns
|
|
||||||
|
|
||||||
| Manufacturer | Pattern | Authentication Required |
|
|
||||||
|--------------|---------|------------------------|
|
|
||||||
| REOLINK | `http://IP:80/cgi-bin/api.cgi?cmd=onvifSnapPic&channel=0` | Yes |
|
|
||||||
| AXIS | `http://IP/onvif-cgi/jpg/image.cgi?resolution=WxH&compression=30` | Yes |
|
|
||||||
| Bosch | `http://IP/snap.jpg?JpegCam=N` | Yes |
|
|
||||||
|
|
||||||
**InvalidAfterConnect/Reboot**:
|
|
||||||
- REOLINK: InvalidAfterConnect=true, InvalidAfterReboot=true
|
|
||||||
- AXIS: All false (persistent URIs)
|
|
||||||
- Bosch: InvalidAfterReboot=true
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bitrate and Frame Rate Analysis
|
|
||||||
|
|
||||||
### REOLINK E1 Zoom
|
|
||||||
- **MainStream**: 1024 kbps @ 15fps (2048x1536)
|
|
||||||
- **SubStream**: 512 kbps @ 15fps (640x480)
|
|
||||||
- **Quality**: 0 (main), 2 (sub)
|
|
||||||
|
|
||||||
### AXIS Q3819-PVE
|
|
||||||
- **H264**: Max bitrate @ 30fps (8192x1728)
|
|
||||||
- **JPEG**: Max bitrate @ 30fps (8192x1728)
|
|
||||||
- **Quality**: 70 for both
|
|
||||||
- **Bitrate Limit**: 2147483647 (max int32 = unlimited)
|
|
||||||
|
|
||||||
### AXIS P3818-PVE
|
|
||||||
- **H264**: Max bitrate @ 30fps (1920x960)
|
|
||||||
- **JPEG**: Max bitrate @ 30fps (5120x2560)
|
|
||||||
- **Quality**: 70 for both
|
|
||||||
- **Bitrate Limit**: 2147483647 (unlimited)
|
|
||||||
|
|
||||||
### Bosch Panoramic 5100i
|
|
||||||
- **Highest**: 13000 kbps @ 30fps (3072x1728 dewarped)
|
|
||||||
- **Lowest**: 400 kbps @ 30fps (512x288)
|
|
||||||
- **Standard**: 5200 kbps @ 30fps (1920x1080)
|
|
||||||
- **Quality**: 50 across all profiles
|
|
||||||
|
|
||||||
### Bosch Starlight 8000i
|
|
||||||
- **H264**: 1400 kbps @ 30fps (1536x864)
|
|
||||||
- **JPEG**: 6000 kbps @ 1fps (1536x864)
|
|
||||||
- **Quality**: 50 (H264), 70 (JPEG)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
### Priority 1: Create Camera-Specific Tests
|
|
||||||
|
|
||||||
Each manufacturer has distinct patterns worthy of dedicated test files:
|
|
||||||
|
|
||||||
1. **reolink_e1_zoom_test.go**
|
|
||||||
- Test PTZ status retrieval
|
|
||||||
- Verify dual-stream profiles
|
|
||||||
- Test CGI-based snapshot URLs
|
|
||||||
- Validate 15fps frame rate limits
|
|
||||||
|
|
||||||
2. **axis_q3819_test.go**
|
|
||||||
- Test ultra-wide resolution (8192x1728)
|
|
||||||
- Verify analytics service
|
|
||||||
- Test dual H264/JPEG encoding
|
|
||||||
- Validate WDR and exposure settings
|
|
||||||
- Test multicast support
|
|
||||||
|
|
||||||
3. **axis_p3818_test.go**
|
|
||||||
- Test 5120x2560 panoramic resolution
|
|
||||||
- Similar to Q3819 but different aspect ratio
|
|
||||||
- Benchmark performance differences
|
|
||||||
|
|
||||||
4. **bosch_panoramic_5100i_test.go**
|
|
||||||
- Test circular (2112x2112) image profiles
|
|
||||||
- Test dewarped profiles
|
|
||||||
- Handle IncompleteConfiguration errors gracefully
|
|
||||||
- Test metadata and audio-only profiles
|
|
||||||
- Test 16 different profiles
|
|
||||||
|
|
||||||
5. **bosch_starlight_8000i_test.go**
|
|
||||||
- Test low-light imaging capabilities
|
|
||||||
- Test I/O connectors (2 inputs, 1 relay output)
|
|
||||||
- Test JPEG motion (1fps) vs H264 (30fps)
|
|
||||||
|
|
||||||
### Priority 2: Cross-Manufacturer Tests
|
|
||||||
|
|
||||||
Create tests that verify common ONVIF compliance:
|
|
||||||
|
|
||||||
1. **stream_uri_compatibility_test.go**
|
|
||||||
- Parse and validate different RTSP URL formats
|
|
||||||
- Test RTSP connection to each pattern
|
|
||||||
- Verify authentication handling
|
|
||||||
|
|
||||||
2. **imaging_settings_test.go**
|
|
||||||
- Test brightness/contrast/saturation ranges
|
|
||||||
- Test optional features (WDR, exposure, white balance)
|
|
||||||
- Verify manufacturer-specific defaults
|
|
||||||
|
|
||||||
3. **profile_enumeration_test.go**
|
|
||||||
- Test handling of 2-16 profiles
|
|
||||||
- Verify profile names and tokens
|
|
||||||
- Test resolution validation
|
|
||||||
|
|
||||||
### Priority 3: Edge Case Tests
|
|
||||||
|
|
||||||
1. **incomplete_profile_handling_test.go**
|
|
||||||
- Test cameras with profiles lacking video encoders
|
|
||||||
- Verify graceful error handling for IncompleteConfiguration
|
|
||||||
- Test metadata-only and audio-only profiles
|
|
||||||
|
|
||||||
2. **performance_benchmark_test.go**
|
|
||||||
- Benchmark GetProfiles (100ms to 800ms variation)
|
|
||||||
- Test response time consistency
|
|
||||||
- Measure concurrent request handling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Patterns for Tests
|
|
||||||
|
|
||||||
### Example: Testing AXIS Cameras
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestAXISQ3819PVE_UltraWideResolution(t *testing.T) {
|
|
||||||
skipIfNoCamera(t)
|
|
||||||
|
|
||||||
client := createTestClient(t)
|
|
||||||
profiles, err := client.GetProfiles()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// AXIS Q3819 should have H264 and JPEG profiles
|
|
||||||
assert.Equal(t, 2, len(profiles))
|
|
||||||
|
|
||||||
// Find H264 profile
|
|
||||||
var h264Profile *onvif.Profile
|
|
||||||
for _, p := range profiles {
|
|
||||||
if p.VideoEncoderConfiguration != nil &&
|
|
||||||
p.VideoEncoderConfiguration.Encoding == "H264" {
|
|
||||||
h264Profile = &p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotNil(t, h264Profile, "H264 profile should exist")
|
|
||||||
|
|
||||||
// Verify ultra-wide resolution
|
|
||||||
assert.Equal(t, 8192, h264Profile.VideoEncoderConfiguration.Resolution.Width)
|
|
||||||
assert.Equal(t, 1728, h264Profile.VideoEncoderConfiguration.Resolution.Height)
|
|
||||||
|
|
||||||
// Verify 30fps
|
|
||||||
assert.Equal(t, 30, h264Profile.VideoEncoderConfiguration.RateControl.FrameRateLimit)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example: Testing Bosch Panoramic Profiles
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestBoschPanoramic5100i_MultipleProfiles(t *testing.T) {
|
|
||||||
skipIfNoCamera(t)
|
|
||||||
|
|
||||||
client := createTestClient(t)
|
|
||||||
profiles, err := client.GetProfiles()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Should have 16 profiles
|
|
||||||
assert.Equal(t, 16, len(profiles))
|
|
||||||
|
|
||||||
// Count profiles with valid video encoders
|
|
||||||
validVideoProfiles := 0
|
|
||||||
for _, p := range profiles {
|
|
||||||
if p.VideoEncoderConfiguration != nil {
|
|
||||||
validVideoProfiles++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, 9, validVideoProfiles, "Should have 9 video profiles")
|
|
||||||
|
|
||||||
// Test that incomplete profiles fail gracefully
|
|
||||||
for _, p := range profiles {
|
|
||||||
uri, err := client.GetStreamURI(p.Token, "RTP-Unicast")
|
|
||||||
|
|
||||||
if p.VideoEncoderConfiguration != nil {
|
|
||||||
// Valid profiles should succeed
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Profile %s failed: %v", p.Token, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Incomplete profiles should fail
|
|
||||||
assert.Error(t, err, "Profile %s should fail (no video encoder)", p.Token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example: Testing PTZ Status
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestREOLINKE1Zoom_PTZStatus(t *testing.T) {
|
|
||||||
skipIfNoCamera(t)
|
|
||||||
|
|
||||||
client := createTestClient(t)
|
|
||||||
profiles, err := client.GetProfiles()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for _, profile := range profiles {
|
|
||||||
if profile.PTZConfiguration != nil {
|
|
||||||
status, err := client.GetPTZStatus(profile.Token)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Should report IDLE when not moving
|
|
||||||
assert.NotNil(t, status.MoveStatus)
|
|
||||||
assert.Contains(t, []string{"IDLE", "MOVING"}, status.MoveStatus.PanTilt)
|
|
||||||
assert.Contains(t, []string{"IDLE", "MOVING"}, status.MoveStatus.Zoom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integration Test Suite Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── manufacturers/
|
|
||||||
│ ├── reolink/
|
|
||||||
│ │ └── e1_zoom_test.go
|
|
||||||
│ ├── axis/
|
|
||||||
│ │ ├── q3819_pve_test.go
|
|
||||||
│ │ └── p3818_pve_test.go
|
|
||||||
│ └── bosch/
|
|
||||||
│ ├── flexidome_indoor_5100i_ir_test.go (existing)
|
|
||||||
│ ├── flexidome_panoramic_5100i_test.go
|
|
||||||
│ └── flexidome_starlight_8000i_test.go
|
|
||||||
├── compliance/
|
|
||||||
│ ├── stream_uri_test.go
|
|
||||||
│ ├── imaging_test.go
|
|
||||||
│ └── profile_test.go
|
|
||||||
├── benchmarks/
|
|
||||||
│ └── response_time_test.go
|
|
||||||
└── edge_cases/
|
|
||||||
├── incomplete_profiles_test.go
|
|
||||||
└── error_handling_test.go
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Insights
|
|
||||||
|
|
||||||
### RTSP Tunnel Parameters (Bosch)
|
|
||||||
|
|
||||||
Bosch uses a proprietary `rtsp_tunnel` endpoint with various parameters:
|
|
||||||
|
|
||||||
- **p**: Profile index (0-15)
|
|
||||||
- **line**: Video source line
|
|
||||||
- 1 = Full image circle
|
|
||||||
- 2 = Dewarped view mode
|
|
||||||
- 3 = Electronic PTZ
|
|
||||||
- **inst**: Stream instance (1-4, corresponds to bitrate tiers)
|
|
||||||
- **h26x**: Codec selection
|
|
||||||
- 0 = JPEG
|
|
||||||
- 4 = H.264
|
|
||||||
- **vcd**: Video coding
|
|
||||||
- 2 = Metadata stream
|
|
||||||
- **von**: Video on (0/1)
|
|
||||||
- **aon**: Audio on (0/1)
|
|
||||||
- **aud**: Audio stream identifier
|
|
||||||
- **JpegCam**: Camera number for snapshots
|
|
||||||
|
|
||||||
### AXIS URL Parameters
|
|
||||||
|
|
||||||
- **profile**: Profile token
|
|
||||||
- **sessiontimeout**: Session timeout in seconds
|
|
||||||
- **streamtype**: unicast or multicast
|
|
||||||
- **resolution**: Snapshot resolution (WxH)
|
|
||||||
- **compression**: JPEG compression quality (0-100, lower = better)
|
|
||||||
|
|
||||||
### REOLINK CGI API
|
|
||||||
|
|
||||||
Uses proprietary CGI commands:
|
|
||||||
- `cmd=onvifSnapPic`: Get ONVIF-compliant snapshot
|
|
||||||
- `channel=0`: Camera channel
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
All cameras require HTTP Digest Authentication for ONVIF requests.
|
|
||||||
|
|
||||||
### TLS Support
|
|
||||||
|
|
||||||
| Camera | TLS 1.1 | TLS 1.2 | Notes |
|
|
||||||
|--------|---------|---------|-------|
|
|
||||||
| REOLINK E1 Zoom | ✗ | ✗ | HTTP only |
|
|
||||||
| AXIS Q3819-PVE | ✓ | ✓ | Full TLS support |
|
|
||||||
| AXIS P3818-PVE | ✓ | ✓ | Full TLS support |
|
|
||||||
| Bosch Panoramic 5100i | ✗ | ✓ | TLS 1.2 only |
|
|
||||||
| Bosch Starlight 8000i | ✓ | ✓ | Full TLS support |
|
|
||||||
|
|
||||||
**Recommendation**: AXIS cameras provide the strongest security posture with IP filtering, access policy config, and TLS support.
|
|
||||||
|
|
||||||
### WS-Security
|
|
||||||
All cameras support WS-Security UsernameToken with digest authentication, as evidenced by successful ONVIF communication.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compatibility Matrix
|
|
||||||
|
|
||||||
### ONVIF Profile Compliance
|
|
||||||
|
|
||||||
Based on feature analysis, likely ONVIF profile compliance:
|
|
||||||
|
|
||||||
| Camera | Profile S | Profile T | Profile G | Profile M |
|
|
||||||
|--------|-----------|-----------|-----------|-----------|
|
|
||||||
| REOLINK E1 Zoom | ✓ | ✓ (PTZ) | ✗ | ✗ |
|
|
||||||
| AXIS Q3819-PVE | ✓ | ✗ | ✓ (Analytics) | ✓ (Metadata) |
|
|
||||||
| AXIS P3818-PVE | ✓ | ✗ | ✓ (Analytics) | ✓ (Metadata) |
|
|
||||||
| Bosch Panoramic 5100i | ✓ | ✓ (ePTZ) | ✓ (Analytics) | ✓ (Metadata) |
|
|
||||||
| Bosch Starlight 8000i | ✓ | ✗ | ✗ | Partial |
|
|
||||||
|
|
||||||
**Profiles**:
|
|
||||||
- **S**: Streaming (basic video)
|
|
||||||
- **T**: PTZ control
|
|
||||||
- **G**: Video analytics
|
|
||||||
- **M**: Metadata streaming
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusions
|
|
||||||
|
|
||||||
### Best Practices Discovered
|
|
||||||
|
|
||||||
1. **Profile Enumeration**: Always check VideoEncoderConfiguration before calling GetStreamURI
|
|
||||||
2. **Error Handling**: Bosch cameras may return IncompleteConfiguration for metadata profiles
|
|
||||||
3. **Response Times**: Expect 5-800ms for GetProfiles depending on camera complexity
|
|
||||||
4. **URL Patterns**: Cannot assume consistent RTSP URL format across manufacturers
|
|
||||||
5. **Imaging Defaults**: Manufacturers use different scales (0-255 vs 0-100 vs 0-128)
|
|
||||||
|
|
||||||
### Client Library Improvements Needed
|
|
||||||
|
|
||||||
1. **URL Parser**: Helper to parse and validate different RTSP URL formats
|
|
||||||
2. **Profile Filter**: Method to filter profiles by capability (video, audio, metadata)
|
|
||||||
3. **Retry Logic**: Handle transient errors and timeouts
|
|
||||||
4. **TLS Support**: Enable HTTPS for cameras supporting TLS
|
|
||||||
5. **Batch Operations**: Parallel GetStreamURI calls for cameras with many profiles
|
|
||||||
|
|
||||||
### Test Coverage Recommendations
|
|
||||||
|
|
||||||
Based on this analysis, create test files covering:
|
|
||||||
|
|
||||||
1. ✅ Bosch FLEXIDOME indoor 5100i IR (already exists)
|
|
||||||
2. 🔲 REOLINK E1 Zoom (PTZ, dual stream)
|
|
||||||
3. 🔲 AXIS Q3819-PVE (ultra-wide, analytics)
|
|
||||||
4. 🔲 AXIS P3818-PVE (panoramic, analytics)
|
|
||||||
5. 🔲 Bosch FLEXIDOME panoramic 5100i (16 profiles, dewarping)
|
|
||||||
6. 🔲 Bosch FLEXIDOME IP starlight 8000i (low-light, I/O)
|
|
||||||
|
|
||||||
### Interoperability Score
|
|
||||||
|
|
||||||
Based on ONVIF compliance, feature richness, and ease of integration:
|
|
||||||
|
|
||||||
| Camera | Score | Rationale |
|
|
||||||
|--------|-------|-----------|
|
|
||||||
| AXIS P3818-PVE | 9.5/10 | Excellent compliance, fast, feature-rich |
|
|
||||||
| AXIS Q3819-PVE | 9.5/10 | Same as P3818, ultra-wide resolution |
|
|
||||||
| Bosch Starlight 8000i | 8.0/10 | Good compliance, moderate features |
|
|
||||||
| Bosch Panoramic 5100i | 7.5/10 | Complex profile structure, some errors |
|
|
||||||
| REOLINK E1 Zoom | 7.0/10 | Basic features, slower responses, limited imaging |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Create manufacturer-specific test files** for each camera model
|
|
||||||
2. **Implement helper functions** for common patterns (URL parsing, profile filtering)
|
|
||||||
3. **Add benchmark tests** to track performance regression
|
|
||||||
4. **Document manufacturer quirks** in code comments
|
|
||||||
5. **Create CI/CD pipeline** to test against real cameras (when available)
|
|
||||||
6. **Expand coverage** for PTZ operations on REOLINK
|
|
||||||
7. **Test analytics** on AXIS cameras
|
|
||||||
8. **Validate TLS connections** on supported cameras
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix: Raw Data Summary
|
|
||||||
|
|
||||||
### REOLINK E1 Zoom
|
|
||||||
- Profiles: 2
|
|
||||||
- Stream URIs: 2/2 successful
|
|
||||||
- Snapshot URIs: 2/2 successful
|
|
||||||
- Video Encoders: 2/2 successful
|
|
||||||
- Imaging Settings: 1/1 successful
|
|
||||||
- PTZ Status: 2/2 successful (both IDLE)
|
|
||||||
- PTZ Presets: 0
|
|
||||||
- Total Errors: 0
|
|
||||||
|
|
||||||
### AXIS Q3819-PVE
|
|
||||||
- Profiles: 2
|
|
||||||
- Stream URIs: 2/2 successful
|
|
||||||
- Snapshot URIs: 2/2 successful
|
|
||||||
- Video Encoders: 2/2 successful
|
|
||||||
- Imaging Settings: 1/1 successful
|
|
||||||
- Total Errors: 0
|
|
||||||
|
|
||||||
### AXIS P3818-PVE
|
|
||||||
- Profiles: 2
|
|
||||||
- Stream URIs: 2/2 successful
|
|
||||||
- Snapshot URIs: 2/2 successful
|
|
||||||
- Video Encoders: 2/2 successful
|
|
||||||
- Imaging Settings: 1/1 successful
|
|
||||||
- Total Errors: 0
|
|
||||||
|
|
||||||
### Bosch FLEXIDOME panoramic 5100i
|
|
||||||
- Profiles: 16
|
|
||||||
- Stream URIs: 13/16 successful (3 IncompleteConfiguration errors)
|
|
||||||
- Snapshot URIs: 16/16 successful
|
|
||||||
- Video Encoders: 9/9 successful (only tested valid profiles)
|
|
||||||
- Imaging Settings: 1/1 successful
|
|
||||||
- Total Errors: 3 (expected for incomplete profiles)
|
|
||||||
|
|
||||||
### Bosch FLEXIDOME IP starlight 8000i
|
|
||||||
- Profiles: 3
|
|
||||||
- Stream URIs: 3/3 successful
|
|
||||||
- Snapshot URIs: 3/3 successful
|
|
||||||
- Video Encoders: 3/3 successful
|
|
||||||
- Imaging Settings: 1/1 successful
|
|
||||||
- Total Errors: 0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**End of Analysis Report**
|
|
||||||
+28
-1
@@ -8,7 +8,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **Simplified Endpoint API**: `NewClient()` now accepts multiple endpoint formats
|
||||||
|
- Simple IP address: `"192.168.1.100"`
|
||||||
|
- IP with port: `"192.168.1.100:8080"`
|
||||||
|
- Full URL: `"http://192.168.1.100/onvif/device_service"` (backward compatible)
|
||||||
|
- Automatically adds `http://` scheme and `/onvif/device_service` path when needed
|
||||||
|
- See `docs/SIMPLIFIED_ENDPOINT.md` for details
|
||||||
|
- **Localhost URL Fix**: Automatic handling of cameras that report localhost addresses
|
||||||
|
- Detects and fixes localhost/127.0.0.1/0.0.0.0/::1 in GetCapabilities response
|
||||||
|
- Replaces with actual camera IP address
|
||||||
|
- Preserves service-specific ports when specified
|
||||||
|
- Handles common camera firmware bugs transparently
|
||||||
|
- Comprehensive test coverage for endpoint normalization (12 test cases)
|
||||||
|
- Comprehensive test coverage for localhost URL handling (10 test cases)
|
||||||
|
- New example: `examples/simplified-endpoint/` demonstrating all endpoint formats
|
||||||
|
- Documentation: `docs/PROJECT_STRUCTURE.md` explaining project organization
|
||||||
- Initial release of go-onvif library
|
- Initial release of go-onvif library
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Project Structure**: Implemented ideal Go project layout
|
||||||
|
- Moved `soap/` to `internal/soap/` (private implementation)
|
||||||
|
- Moved `test/test-server.go` to `examples/test-server/` for clarity
|
||||||
|
- Removed empty `test/` directory
|
||||||
|
- Public API remains at root level for clean imports
|
||||||
|
- Follows Standard Go Project Layout for libraries
|
||||||
|
- Updated all imports throughout codebase
|
||||||
|
- See `docs/PROJECT_STRUCTURE.md` and `docs/ARCHITECTURE.md` for details
|
||||||
|
- Updated `docs/ARCHITECTURE.md` to reflect new project structure
|
||||||
|
- Updated module path from `github.com/0x524A/onvif-go` to `github.com/0x524a/onvif-go` (lowercase)
|
||||||
- ONVIF Client with context support
|
- ONVIF Client with context support
|
||||||
- Device service implementation
|
- Device service implementation
|
||||||
- GetDeviceInformation
|
- GetDeviceInformation
|
||||||
@@ -48,4 +75,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Comprehensive documentation
|
- Comprehensive documentation
|
||||||
- README with usage guide
|
- README with usage guide
|
||||||
|
|
||||||
[Unreleased]: https://github.com/0x524A/go-onvif/compare/v0.1.0...HEAD
|
[Unreleased]: https://github.com/0x524a/onvif-go/compare/v0.1.0...HEAD
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ git clone https://github.com/YOUR_USERNAME/go-onvif.git
|
|||||||
cd go-onvif
|
cd go-onvif
|
||||||
|
|
||||||
# Add upstream remote
|
# Add upstream remote
|
||||||
git remote add upstream https://github.com/0x524A/go-onvif.git
|
git remote add upstream https://github.com/0x524a/onvif-go.git
|
||||||
|
|
||||||
# Create a branch
|
# Create a branch
|
||||||
git checkout -b feature/my-new-feature
|
git checkout -b feature/my-new-feature
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
# Linting Fixes - golangci-lint Issues Resolved
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
All 7 linting errors reported by golangci-lint have been successfully fixed.
|
|
||||||
|
|
||||||
## Issues Fixed
|
|
||||||
|
|
||||||
### 1. Unchecked Error Return: `rand.Read`
|
|
||||||
**File:** `soap/soap.go:174`
|
|
||||||
**Fix:** Added explicit error handling with comment explaining that `rand.Read` from `crypto/rand` always succeeds for valid buffer sizes.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
rand.Read(nonceBytes)
|
|
||||||
|
|
||||||
// After
|
|
||||||
_, _ = rand.Read(nonceBytes) // rand.Read always returns len(nonceBytes), nil
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Unchecked Error Return: `w.Write`
|
|
||||||
**File:** `client_test.go:102`
|
|
||||||
**Fix:** Added explicit error handling for `http.ResponseWriter.Write()` with explanatory comment.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
w.Write([]byte(response))
|
|
||||||
|
|
||||||
// After
|
|
||||||
_, _ = w.Write([]byte(response)) // Writing to ResponseWriter; error is handled by http package
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3-5. Unchecked Error Return: `client.Initialize`
|
|
||||||
**Files:**
|
|
||||||
- `cmd/onvif-quick/main.go:121`
|
|
||||||
- `cmd/onvif-quick/main.go:164`
|
|
||||||
- `cmd/onvif-quick/main.go:269`
|
|
||||||
|
|
||||||
**Fix:** Added explicit error ignoring with explanatory comments. Errors are caught in subsequent operations.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
client.Initialize(ctx)
|
|
||||||
|
|
||||||
// After
|
|
||||||
_ = client.Initialize(ctx) // Ignore initialization errors, we'll catch them on GetProfiles
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Unchecked Error Return: `client.Stop`
|
|
||||||
**File:** `cmd/onvif-quick/main.go:226`
|
|
||||||
**Fix:** Added explicit error handling for PTZ stop operation.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
client.Stop(ctx, profileToken, true, false)
|
|
||||||
|
|
||||||
// After
|
|
||||||
_ = client.Stop(ctx, profileToken, true, false) // Stop PTZ movement
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Unused Field: `deviceEndpoint`
|
|
||||||
**File:** `client.go:21`
|
|
||||||
**Fix:** Removed the unused field from the `Client` struct.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
type Client struct {
|
|
||||||
deviceEndpoint string
|
|
||||||
mediaEndpoint string
|
|
||||||
ptzEndpoint string
|
|
||||||
imagingEndpoint string
|
|
||||||
eventEndpoint string
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
type Client struct {
|
|
||||||
mediaEndpoint string
|
|
||||||
ptzEndpoint string
|
|
||||||
imagingEndpoint string
|
|
||||||
eventEndpoint string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8-10. Unchecked Error Return: Deferred `Close()` calls
|
|
||||||
**Files:**
|
|
||||||
- `client_test.go:59` - `r.Body.Close()`
|
|
||||||
- `discovery/discovery.go:81` - `conn.Close()`
|
|
||||||
- `soap/soap.go:128` - `resp.Body.Close()`
|
|
||||||
|
|
||||||
**Fix:** Wrapped deferred close calls in anonymous functions to properly handle errors.
|
|
||||||
```go
|
|
||||||
// Before
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// After
|
|
||||||
defer func() { _ = conn.Close() }()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
### Linting Results
|
|
||||||
```bash
|
|
||||||
$ golangci-lint run --timeout=5m
|
|
||||||
0 issues.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
All tests continue to pass:
|
|
||||||
```bash
|
|
||||||
$ go test -v ./...
|
|
||||||
PASS
|
|
||||||
ok github.com/0x524A/go-onvif 30.008s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Results
|
|
||||||
Both CLI tools build successfully:
|
|
||||||
```bash
|
|
||||||
$ make build
|
|
||||||
🔨 Building ONVIF CLI...
|
|
||||||
🔨 Building ONVIF Quick Tool...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices Applied
|
|
||||||
|
|
||||||
1. **Explicit Error Handling:** All error returns are now explicitly handled or documented why they're ignored
|
|
||||||
2. **Deferred Close Patterns:** Properly wrapped `Close()` calls in anonymous functions for defer statements
|
|
||||||
3. **Code Cleanliness:** Removed unused struct fields to reduce code bloat
|
|
||||||
4. **Documentation:** Added inline comments explaining why certain errors are explicitly ignored
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
- ✅ No functional changes to the library behavior
|
|
||||||
- ✅ All tests still pass
|
|
||||||
- ✅ CLI tools compile and work correctly
|
|
||||||
- ✅ Code now follows Go best practices and linting standards
|
|
||||||
- ✅ Ready for CI/CD pipelines with strict linting requirements
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Go ONVIF Library Makefile
|
# ONVIF GO Library Makefile
|
||||||
|
|
||||||
.PHONY: all build test clean install deps lint fmt vet check examples cli docker
|
.PHONY: all build test clean install deps lint fmt vet check examples cli docker
|
||||||
|
|
||||||
@@ -96,34 +96,92 @@ examples:
|
|||||||
go build -o $(BINARY_DIR)/examples/ptz ./examples/ptz
|
go build -o $(BINARY_DIR)/examples/ptz ./examples/ptz
|
||||||
|
|
||||||
# Build for multiple platforms
|
# Build for multiple platforms
|
||||||
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
|
LDFLAGS := -ldflags "-s -w -X main.Version=$(VERSION)"
|
||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
@echo "🌍 Building for multiple platforms..."
|
@echo "🌍 Building for multiple platforms (version: $(VERSION))..."
|
||||||
@mkdir -p $(BINARY_DIR)
|
@mkdir -p $(BINARY_DIR)
|
||||||
|
|
||||||
# Linux AMD64
|
# Linux AMD64
|
||||||
GOOS=linux GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-cli-linux-amd64 ./cmd/onvif-cli
|
@echo "Building Linux AMD64..."
|
||||||
GOOS=linux GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-quick-linux-amd64 ./cmd/onvif-quick
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-linux-amd64 ./cmd/onvif-cli
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-linux-amd64 ./cmd/onvif-quick
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-server-linux-amd64 ./cmd/onvif-server
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-diagnostics-linux-amd64 ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
# Linux ARM64
|
# Linux ARM64
|
||||||
GOOS=linux GOARCH=arm64 go build -o $(BINARY_DIR)/onvif-cli-linux-arm64 ./cmd/onvif-cli
|
@echo "Building Linux ARM64..."
|
||||||
GOOS=linux GOARCH=arm64 go build -o $(BINARY_DIR)/onvif-quick-linux-arm64 ./cmd/onvif-quick
|
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-linux-arm64 ./cmd/onvif-cli
|
||||||
|
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-linux-arm64 ./cmd/onvif-quick
|
||||||
|
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-server-linux-arm64 ./cmd/onvif-server
|
||||||
|
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-diagnostics-linux-arm64 ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
|
# Linux ARM (32-bit)
|
||||||
|
@echo "Building Linux ARM..."
|
||||||
|
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-linux-arm ./cmd/onvif-cli
|
||||||
|
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-linux-arm ./cmd/onvif-quick
|
||||||
|
|
||||||
# Windows AMD64
|
# Windows AMD64
|
||||||
GOOS=windows GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-cli-windows-amd64.exe ./cmd/onvif-cli
|
@echo "Building Windows AMD64..."
|
||||||
GOOS=windows GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-quick-windows-amd64.exe ./cmd/onvif-quick
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-windows-amd64.exe ./cmd/onvif-cli
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-windows-amd64.exe ./cmd/onvif-quick
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-server-windows-amd64.exe ./cmd/onvif-server
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-diagnostics-windows-amd64.exe ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
# macOS AMD64
|
# Windows ARM64
|
||||||
GOOS=darwin GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-cli-darwin-amd64 ./cmd/onvif-cli
|
@echo "Building Windows ARM64..."
|
||||||
GOOS=darwin GOARCH=amd64 go build -o $(BINARY_DIR)/onvif-quick-darwin-amd64 ./cmd/onvif-quick
|
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-windows-arm64.exe ./cmd/onvif-cli
|
||||||
|
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-windows-arm64.exe ./cmd/onvif-quick
|
||||||
|
|
||||||
|
# macOS AMD64 (Intel)
|
||||||
|
@echo "Building macOS AMD64..."
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-darwin-amd64 ./cmd/onvif-cli
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-darwin-amd64 ./cmd/onvif-quick
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-server-darwin-amd64 ./cmd/onvif-server
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-diagnostics-darwin-amd64 ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
# macOS ARM64 (Apple Silicon)
|
# macOS ARM64 (Apple Silicon)
|
||||||
GOOS=darwin GOARCH=arm64 go build -o $(BINARY_DIR)/onvif-cli-darwin-arm64 ./cmd/onvif-cli
|
@echo "Building macOS ARM64..."
|
||||||
GOOS=darwin GOARCH=arm64 go build -o $(BINARY_DIR)/onvif-quick-darwin-arm64 ./cmd/onvif-quick
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-cli-darwin-arm64 ./cmd/onvif-cli
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-quick-darwin-arm64 ./cmd/onvif-quick
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-server-darwin-arm64 ./cmd/onvif-server
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(BINARY_DIR)/onvif-diagnostics-darwin-arm64 ./cmd/onvif-diagnostics
|
||||||
|
|
||||||
|
@echo "✅ All binaries built successfully in $(BINARY_DIR)/"
|
||||||
|
@echo ""
|
||||||
|
@ls -lh $(BINARY_DIR)/
|
||||||
|
|
||||||
|
# Create release archives with checksums
|
||||||
|
release: build-all
|
||||||
|
@echo "📦 Creating release archives..."
|
||||||
|
@mkdir -p releases
|
||||||
|
|
||||||
|
# Create archives for each platform
|
||||||
|
@cd $(BINARY_DIR) && \
|
||||||
|
for os in linux darwin windows; do \
|
||||||
|
for arch in amd64 arm64 arm; do \
|
||||||
|
if [ "$$os" = "windows" ] && [ "$$arch" != "arm" ]; then \
|
||||||
|
if [ -f onvif-cli-$$os-$$arch.exe ]; then \
|
||||||
|
zip -j ../releases/go-onvif-$(VERSION)-$$os-$$arch.zip onvif-*-$$os-$$arch.exe ../README.md ../LICENSE 2>/dev/null || true; \
|
||||||
|
fi; \
|
||||||
|
elif [ "$$os" != "windows" ]; then \
|
||||||
|
if [ -f onvif-cli-$$os-$$arch ]; then \
|
||||||
|
tar czf ../releases/go-onvif-$(VERSION)-$$os-$$arch.tar.gz onvif-*-$$os-$$arch ../README.md ../LICENSE 2>/dev/null || true; \
|
||||||
|
fi; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
@cd releases && sha256sum * > checksums.txt 2>/dev/null || shasum -a 256 * > checksums.txt
|
||||||
|
@echo "✅ Release archives created in releases/"
|
||||||
|
@ls -lh releases/
|
||||||
|
|
||||||
# Create Docker image
|
# Create Docker image
|
||||||
docker:
|
docker:
|
||||||
@echo "🐳 Building Docker image..."
|
@echo "🐳 Building Docker image..."
|
||||||
docker build -t go-onvif:latest .
|
docker build -t onvif-go:latest .
|
||||||
|
|
||||||
# Development setup
|
# Development setup
|
||||||
dev-setup:
|
dev-setup:
|
||||||
|
|||||||
+12
-9
@@ -5,7 +5,7 @@ Get up and running with go-onvif in 5 minutes!
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/0x524A/go-onvif
|
go get github.com/0x524a/onvif-go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 1: Discover Cameras
|
## Step 1: Discover Cameras
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -42,7 +42,7 @@ func main() {
|
|||||||
|
|
||||||
## Step 2: Connect to Camera
|
## Step 2: Connect to Camera
|
||||||
|
|
||||||
Create a client and get basic information:
|
Create a client and get basic information. The endpoint can be specified in multiple formats:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -52,13 +52,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create client
|
// Create client - endpoint accepts multiple formats:
|
||||||
|
// - Simple IP: "192.168.1.100"
|
||||||
|
// - IP with port: "192.168.1.100:8080"
|
||||||
|
// - Full URL: "http://192.168.1.100/onvif/device_service"
|
||||||
client, err := onvif.NewClient(
|
client, err := onvif.NewClient(
|
||||||
"http://192.168.1.100/onvif/device_service",
|
"192.168.1.100", // Simple IP address works!
|
||||||
onvif.WithCredentials("admin", "password"),
|
onvif.WithCredentials("admin", "password"),
|
||||||
onvif.WithTimeout(30*time.Second),
|
onvif.WithTimeout(30*time.Second),
|
||||||
)
|
)
|
||||||
@@ -178,7 +181,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -262,9 +265,9 @@ func main() {
|
|||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. **Explore Examples**: Check out the `examples/` directory for more detailed use cases
|
1. **Explore Examples**: Check out the `examples/` directory for more detailed use cases
|
||||||
2. **Read Documentation**: Visit [pkg.go.dev](https://pkg.go.dev/github.com/0x524A/go-onvif)
|
2. **Read Documentation**: Visit [pkg.go.dev](https://pkg.go.dev/github.com/0x524a/onvif-go)
|
||||||
3. **Review Architecture**: See [ARCHITECTURE.md](ARCHITECTURE.md) for design details
|
3. **Review Architecture**: See [ARCHITECTURE.md](ARCHITECTURE.md) for design details
|
||||||
4. **Check Issues**: Look at [GitHub Issues](https://github.com/0x524A/go-onvif/issues) for known issues
|
4. **Check Issues**: Look at [GitHub Issues](https://github.com/0x524a/onvif-go/issues) for known issues
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# go-onvif - ONVIF Client and Server Library for Go
|
# onvif-go - ONVIF Client and Server Library for Go
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/0x524A/go-onvif)
|
[](https://pkg.go.dev/github.com/0x524a/onvif-go)
|
||||||
[](https://goreportcard.com/report/github.com/0x524A/go-onvif)
|
[](https://goreportcard.com/report/github.com/0x524a/onvif-go)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/0x524A/go-onvif/stargazers)
|
[](https://github.com/0x524a/onvif-go/stargazers)
|
||||||
[](https://github.com/0x524A/go-onvif/issues)
|
[](https://github.com/0x524a/onvif-go/issues)
|
||||||
|
|
||||||
> **Modern, high-performance Go library for ONVIF IP camera integration** - Control surveillance cameras, NVRs, and video devices with comprehensive ONVIF Profile S/T/G support. Includes both client and server implementations for complete ONVIF camera simulation and testing.
|
> **Modern, high-performance Go library for ONVIF IP camera integration** - Control surveillance cameras, NVRs, and video devices with comprehensive ONVIF Profile S/T/G support. Includes both client and server implementations for complete ONVIF camera simulation and testing.
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ ONVIF (Open Network Video Interface Forum) is an open industry standard for IP-b
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/0x524A/go-onvif
|
go get github.com/0x524a/onvif-go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -87,7 +87,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -118,13 +118,16 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create client
|
// Create client - endpoint can be:
|
||||||
|
// - Full URL: "http://192.168.1.100/onvif/device_service"
|
||||||
|
// - IP with port: "192.168.1.100:8080"
|
||||||
|
// - IP only: "192.168.1.100" (automatically adds http:// and path)
|
||||||
client, err := onvif.NewClient(
|
client, err := onvif.NewClient(
|
||||||
"http://192.168.1.100/onvif/device_service",
|
"192.168.1.100", // Simple IP address
|
||||||
onvif.WithCredentials("admin", "password"),
|
onvif.WithCredentials("admin", "password"),
|
||||||
onvif.WithTimeout(30*time.Second),
|
onvif.WithTimeout(30*time.Second),
|
||||||
)
|
)
|
||||||
@@ -319,7 +322,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -379,7 +382,7 @@ go run main.go
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
go-onvif/
|
onvif-go/
|
||||||
├── client.go # Main ONVIF client
|
├── client.go # Main ONVIF client
|
||||||
├── types.go # ONVIF data types
|
├── types.go # ONVIF data types
|
||||||
├── errors.go # Error definitions
|
├── errors.go # Error definitions
|
||||||
@@ -526,14 +529,14 @@ go test -v ./testdata/captures/
|
|||||||
|
|
||||||
If you find this project useful, please consider giving it a star! ⭐
|
If you find this project useful, please consider giving it a star! ⭐
|
||||||
|
|
||||||
[](https://star-history.com/#0x524A/go-onvif&Date)
|
[](https://star-history.com/#0x524a/go-onvif&Date)
|
||||||
|
|
||||||
## 📊 Project Stats
|
## 📊 Project Stats
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -547,9 +550,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- 📖 [Documentation](https://pkg.go.dev/github.com/0x524A/go-onvif)
|
- 📖 [Documentation](https://pkg.go.dev/github.com/0x524a/onvif-go)
|
||||||
- 🐛 [Issue Tracker](https://github.com/0x524A/go-onvif/issues)
|
- 🐛 [Issue Tracker](https://github.com/0x524a/onvif-go/issues)
|
||||||
- 💬 [Discussions](https://github.com/0x524A/go-onvif/discussions)
|
- 💬 [Discussions](https://github.com/0x524a/onvif-go/discussions)
|
||||||
- 🔒 [Security Policy](.github/SECURITY.md)
|
- 🔒 [Security Policy](.github/SECURITY.md)
|
||||||
|
|
||||||
## Keywords
|
## Keywords
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
# Unit Test Coverage Report
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Added comprehensive unit tests to increase code coverage across the go-onvif library.
|
|
||||||
|
|
||||||
## Coverage Improvements
|
|
||||||
|
|
||||||
### Before
|
|
||||||
- Main package (`onvif`): 8.1%
|
|
||||||
- Discovery package: 0%
|
|
||||||
- SOAP package: 0%
|
|
||||||
- **Overall**: ~3% average
|
|
||||||
|
|
||||||
### After
|
|
||||||
- Main package (`onvif`): **19.9%** ✅ (+11.8%)
|
|
||||||
- Discovery package: **67.2%** ✅ (+67.2%)
|
|
||||||
- SOAP package: **81.5%** ✅ (+81.5%)
|
|
||||||
- **Overall**: ~56% average (+53%)
|
|
||||||
|
|
||||||
## Test Files Created
|
|
||||||
|
|
||||||
### 1. `/workspaces/go-onvif/soap/soap_test.go` (297 lines)
|
|
||||||
Comprehensive tests for the SOAP client package:
|
|
||||||
- `TestNewClient` - Client creation with/without credentials
|
|
||||||
- `TestBuildEnvelope` - SOAP envelope generation
|
|
||||||
- `TestClientCall` - HTTP request handling with multiple scenarios:
|
|
||||||
- Successful request
|
|
||||||
- Unauthorized request (401)
|
|
||||||
- HTTP error status (500)
|
|
||||||
- `TestClientCallWithTimeout` - Context timeout behavior
|
|
||||||
- `TestSecurityHeaderCreation` - WS-Security header validation
|
|
||||||
- `BenchmarkNewClient` - Performance: Client creation
|
|
||||||
- `BenchmarkBuildEnvelope` - Performance: Envelope building
|
|
||||||
- `BenchmarkCall` - Performance: SOAP calls
|
|
||||||
|
|
||||||
**Coverage**: 81.5%
|
|
||||||
|
|
||||||
### 2. `/workspaces/go-onvif/discovery/discovery_test.go` (194 lines)
|
|
||||||
Unit tests for the WS-Discovery package:
|
|
||||||
- `TestDevice_GetName` - Device name extraction from scopes
|
|
||||||
- `TestDevice_GetDeviceEndpoint` - Endpoint extraction from XAddrs
|
|
||||||
- `TestDevice_GetLocation` - Location extraction from scopes
|
|
||||||
- `TestDiscover_WithTimeout` - Discovery with timeout
|
|
||||||
- `TestDiscover_InvalidDuration` - Edge case: zero duration
|
|
||||||
- `TestParseSpaceSeparated` - Utility function testing
|
|
||||||
- `TestDevice_GetTypes` - Device type validation
|
|
||||||
- `TestDevice_GetScopes` - Scope parsing
|
|
||||||
- `BenchmarkDeviceGetName` - Performance: Name extraction
|
|
||||||
- `BenchmarkDeviceGetDeviceEndpoint` - Performance: Endpoint extraction
|
|
||||||
|
|
||||||
**Coverage**: 67.2%
|
|
||||||
|
|
||||||
### 3. `/workspaces/go-onvif/device_test.go` (398 lines)
|
|
||||||
Unit tests for the main ONVIF device service:
|
|
||||||
- `TestGetDeviceInformation` - Device info retrieval (success & fault cases)
|
|
||||||
- `TestGetCapabilities` - Capabilities retrieval
|
|
||||||
- `TestGetHostname` - Hostname retrieval
|
|
||||||
- `TestSetHostname` - Hostname modification
|
|
||||||
- `TestGetDNS` - DNS configuration retrieval
|
|
||||||
- `TestGetUsers` - User account listing
|
|
||||||
- `TestCreateUsers` - User creation
|
|
||||||
- `TestDeleteUsers` - User deletion
|
|
||||||
- `TestGetNetworkInterfaces` - Network interface configuration
|
|
||||||
- `BenchmarkDeviceGetDeviceInformation` - Performance: Device info
|
|
||||||
|
|
||||||
**Coverage**: 19.9% (main package also includes media, ptz, imaging which need additional tests)
|
|
||||||
|
|
||||||
## Test Patterns Used
|
|
||||||
|
|
||||||
### 1. Table-Driven Tests
|
|
||||||
```go
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
handler http.HandlerFunc
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"success case", successHandler, false},
|
|
||||||
{"error case", errorHandler, true},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Mock HTTP Servers
|
|
||||||
```go
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
response := `<?xml version="1.0"?>...</xml>`
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(response))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Context Testing
|
|
||||||
```go
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Benchmark Tests
|
|
||||||
```go
|
|
||||||
func BenchmarkOperation(b *testing.B) {
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
operation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps (Optional)
|
|
||||||
|
|
||||||
To achieve higher coverage (>80% overall), consider adding tests for:
|
|
||||||
|
|
||||||
1. **Media Service** (`media.go`)
|
|
||||||
- GetProfiles
|
|
||||||
- GetStreamURI
|
|
||||||
- GetSnapshotURI
|
|
||||||
- Video encoder configuration
|
|
||||||
|
|
||||||
2. **PTZ Service** (`ptz.go`)
|
|
||||||
- ContinuousMove
|
|
||||||
- AbsoluteMove
|
|
||||||
- RelativeMove
|
|
||||||
- Presets management
|
|
||||||
|
|
||||||
3. **Imaging Service** (`imaging.go`)
|
|
||||||
- Imaging settings
|
|
||||||
- Video source configuration
|
|
||||||
|
|
||||||
4. **Server Package** (`server/`)
|
|
||||||
- Server initialization
|
|
||||||
- SOAP handler
|
|
||||||
- Service endpoints
|
|
||||||
|
|
||||||
5. **Integration Tests**
|
|
||||||
- End-to-end workflows
|
|
||||||
- Multi-service interactions
|
|
||||||
- Real camera simulation
|
|
||||||
|
|
||||||
## Testing Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
go test -cover ./...
|
|
||||||
|
|
||||||
# Generate detailed coverage report
|
|
||||||
go test -coverprofile=coverage.out ./...
|
|
||||||
go tool cover -html=coverage.out
|
|
||||||
|
|
||||||
# Run specific package tests
|
|
||||||
go test ./soap/
|
|
||||||
go test ./discovery/
|
|
||||||
go test .
|
|
||||||
|
|
||||||
# Run benchmarks
|
|
||||||
go test -bench=. ./soap/
|
|
||||||
go test -bench=. ./discovery/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
✅ **Linting**: Clean (all previous linting errors fixed)
|
|
||||||
✅ **Build**: Passes
|
|
||||||
✅ **Tests**: All passing
|
|
||||||
✅ **Coverage**: Increased from ~3% to ~56% average
|
|
||||||
✅ **Quality**: Production-ready with comprehensive test coverage
|
|
||||||
|
|
||||||
The library now has:
|
|
||||||
- Strong test coverage for core SOAP functionality
|
|
||||||
- Good coverage for device discovery
|
|
||||||
- Foundation for device service testing
|
|
||||||
- Benchmark tests for performance monitoring
|
|
||||||
- Patterns that can be extended to other services
|
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# build-release.sh - Build release binaries locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION=${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}
|
||||||
|
echo "Building release binaries for version: $VERSION"
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
rm -rf bin releases
|
||||||
|
mkdir -p bin releases
|
||||||
|
|
||||||
|
# Platforms to build
|
||||||
|
PLATFORMS=(
|
||||||
|
"linux/amd64"
|
||||||
|
"linux/arm64"
|
||||||
|
"linux/arm"
|
||||||
|
"windows/amd64"
|
||||||
|
"windows/arm64"
|
||||||
|
"darwin/amd64"
|
||||||
|
"darwin/arm64"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Binaries to build
|
||||||
|
BINARIES=(
|
||||||
|
"onvif-cli"
|
||||||
|
"onvif-quick"
|
||||||
|
"onvif-server"
|
||||||
|
"onvif-diagnostics"
|
||||||
|
)
|
||||||
|
|
||||||
|
LDFLAGS="-s -w -X main.Version=${VERSION} -X main.Commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
|
||||||
|
|
||||||
|
echo "Building binaries..."
|
||||||
|
for platform in "${PLATFORMS[@]}"; do
|
||||||
|
OS="${platform%/*}"
|
||||||
|
ARCH="${platform#*/}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Building for $OS/$ARCH..."
|
||||||
|
|
||||||
|
for binary in "${BINARIES[@]}"; do
|
||||||
|
OUTPUT="bin/${binary}-${OS}-${ARCH}"
|
||||||
|
|
||||||
|
if [ "$OS" = "windows" ]; then
|
||||||
|
OUTPUT="${OUTPUT}.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " - ${binary}"
|
||||||
|
GOOS=$OS GOARCH=$ARCH CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" -o "$OUTPUT" "./cmd/${binary}" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Skipped (build failed)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Creating release archives..."
|
||||||
|
|
||||||
|
cd bin
|
||||||
|
|
||||||
|
for platform in "${PLATFORMS[@]}"; do
|
||||||
|
OS="${platform%/*}"
|
||||||
|
ARCH="${platform#*/}"
|
||||||
|
ARCHIVE_NAME="onvif-go-${VERSION}-${OS}-${ARCH}"
|
||||||
|
|
||||||
|
# Check if any binary exists for this platform
|
||||||
|
if [ "$OS" = "windows" ]; then
|
||||||
|
FILES=(*-${OS}-${ARCH}.exe)
|
||||||
|
else
|
||||||
|
FILES=(*-${OS}-${ARCH})
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip if no files found
|
||||||
|
if [ "${FILES[0]}" = "*-${OS}-${ARCH}" ] || [ "${FILES[0]}" = "*-${OS}-${ARCH}.exe" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Creating archive for ${OS}/${ARCH}..."
|
||||||
|
|
||||||
|
if [ "$OS" = "windows" ]; then
|
||||||
|
# ZIP for Windows
|
||||||
|
zip -q "../releases/${ARCHIVE_NAME}.zip" *-${OS}-${ARCH}.exe ../README.md ../LICENSE
|
||||||
|
else
|
||||||
|
# tar.gz for Unix-like
|
||||||
|
tar czf "../releases/${ARCHIVE_NAME}.tar.gz" *-${OS}-${ARCH} -C .. README.md LICENSE
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Generating checksums..."
|
||||||
|
cd releases
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum * > checksums.txt
|
||||||
|
else
|
||||||
|
shasum -a 256 * > checksums.txt
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Binaries in: $(pwd)/bin/"
|
||||||
|
echo "Archives in: $(pwd)/releases/"
|
||||||
|
echo ""
|
||||||
|
ls -lh releases/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To create a GitHub release, run:"
|
||||||
|
echo " gh release create ${VERSION} releases/* --title \"Release ${VERSION}\" --notes \"Release notes here\""
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -50,18 +51,19 @@ func WithCredentials(username, password string) ClientOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new ONVIF client
|
// NewClient creates a new ONVIF client
|
||||||
|
// The endpoint can be provided in multiple formats:
|
||||||
|
// - Full URL: "http://192.168.1.100/onvif/device_service"
|
||||||
|
// - IP with port: "192.168.1.100:80" (http assumed, /onvif/device_service added)
|
||||||
|
// - IP only: "192.168.1.100" (http://IP:80/onvif/device_service used)
|
||||||
func NewClient(endpoint string, opts ...ClientOption) (*Client, error) {
|
func NewClient(endpoint string, opts ...ClientOption) (*Client, error) {
|
||||||
// Validate endpoint
|
// Normalize endpoint to full URL
|
||||||
parsedURL, err := url.Parse(endpoint)
|
normalizedEndpoint, err := normalizeEndpoint(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid endpoint: %w", err)
|
return nil, fmt.Errorf("invalid endpoint: %w", err)
|
||||||
}
|
}
|
||||||
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
|
||||||
return nil, fmt.Errorf("invalid endpoint: must include scheme and host")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
endpoint: endpoint,
|
endpoint: normalizedEndpoint,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
@@ -80,6 +82,80 @@ func NewClient(endpoint string, opts ...ClientOption) (*Client, error) {
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeEndpoint converts various endpoint formats to a full ONVIF URL
|
||||||
|
func normalizeEndpoint(endpoint string) (string, error) {
|
||||||
|
// Check if endpoint starts with a scheme
|
||||||
|
if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
|
||||||
|
// Parse as full URL
|
||||||
|
parsedURL, err := url.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if parsedURL.Host == "" {
|
||||||
|
return "", fmt.Errorf("URL missing host")
|
||||||
|
}
|
||||||
|
// If path is empty or just "/", add default ONVIF path
|
||||||
|
if parsedURL.Path == "" || parsedURL.Path == "/" {
|
||||||
|
parsedURL.Path = "/onvif/device_service"
|
||||||
|
}
|
||||||
|
return parsedURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No scheme - treat as IP, IP:port, hostname, or hostname:port
|
||||||
|
// Add http:// scheme and validate
|
||||||
|
fullURL := "http://" + endpoint + "/onvif/device_service"
|
||||||
|
parsedURL, err := url.Parse(fullURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid IP address or hostname: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Host == "" {
|
||||||
|
return "", fmt.Errorf("invalid endpoint format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixLocalhostURL replaces localhost/loopback addresses in service URLs with the actual camera host
|
||||||
|
// Some cameras incorrectly report localhost (127.0.0.1, 0.0.0.0, localhost) in their capability URLs
|
||||||
|
func (c *Client) fixLocalhostURL(serviceURL string) string {
|
||||||
|
if serviceURL == "" {
|
||||||
|
return serviceURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the service URL
|
||||||
|
parsedService, err := url.Parse(serviceURL)
|
||||||
|
if err != nil {
|
||||||
|
return serviceURL // Return original if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the service URL has a localhost/loopback address
|
||||||
|
host := parsedService.Hostname()
|
||||||
|
if host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" || host == "::1" {
|
||||||
|
// Parse the client's endpoint to get the actual camera address
|
||||||
|
parsedClient, err := url.Parse(c.endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return serviceURL // Return original if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the host but keep the port from service URL if specified
|
||||||
|
servicePort := parsedService.Port()
|
||||||
|
if servicePort != "" {
|
||||||
|
parsedService.Host = parsedClient.Hostname() + ":" + servicePort
|
||||||
|
} else {
|
||||||
|
parsedService.Host = parsedClient.Hostname()
|
||||||
|
// Use client's port if service doesn't specify one
|
||||||
|
if clientPort := parsedClient.Port(); clientPort != "" {
|
||||||
|
parsedService.Host = parsedClient.Hostname() + ":" + clientPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedService.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceURL
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize discovers and initializes service endpoints
|
// Initialize discovers and initializes service endpoints
|
||||||
func (c *Client) Initialize(ctx context.Context) error {
|
func (c *Client) Initialize(ctx context.Context) error {
|
||||||
// Get device information and capabilities
|
// Get device information and capabilities
|
||||||
@@ -88,18 +164,19 @@ func (c *Client) Initialize(ctx context.Context) error {
|
|||||||
return fmt.Errorf("failed to get capabilities: %w", err)
|
return fmt.Errorf("failed to get capabilities: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract service endpoints
|
// Extract service endpoints and fix any localhost addresses
|
||||||
|
// Some cameras incorrectly report localhost instead of their actual IP
|
||||||
if capabilities.Media != nil && capabilities.Media.XAddr != "" {
|
if capabilities.Media != nil && capabilities.Media.XAddr != "" {
|
||||||
c.mediaEndpoint = capabilities.Media.XAddr
|
c.mediaEndpoint = c.fixLocalhostURL(capabilities.Media.XAddr)
|
||||||
}
|
}
|
||||||
if capabilities.PTZ != nil && capabilities.PTZ.XAddr != "" {
|
if capabilities.PTZ != nil && capabilities.PTZ.XAddr != "" {
|
||||||
c.ptzEndpoint = capabilities.PTZ.XAddr
|
c.ptzEndpoint = c.fixLocalhostURL(capabilities.PTZ.XAddr)
|
||||||
}
|
}
|
||||||
if capabilities.Imaging != nil && capabilities.Imaging.XAddr != "" {
|
if capabilities.Imaging != nil && capabilities.Imaging.XAddr != "" {
|
||||||
c.imagingEndpoint = capabilities.Imaging.XAddr
|
c.imagingEndpoint = c.fixLocalhostURL(capabilities.Imaging.XAddr)
|
||||||
}
|
}
|
||||||
if capabilities.Events != nil && capabilities.Events.XAddr != "" {
|
if capabilities.Events != nil && capabilities.Events.XAddr != "" {
|
||||||
c.eventEndpoint = capabilities.Events.XAddr
|
c.eventEndpoint = c.fixLocalhostURL(capabilities.Events.XAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+312
@@ -5,11 +5,169 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNormalizeEndpoint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full URL with path",
|
||||||
|
input: "http://192.168.1.100/onvif/device_service",
|
||||||
|
expected: "http://192.168.1.100/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full URL with port and path",
|
||||||
|
input: "http://192.168.1.100:8080/onvif/device_service",
|
||||||
|
expected: "http://192.168.1.100:8080/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full URL without path",
|
||||||
|
input: "http://192.168.1.100",
|
||||||
|
expected: "http://192.168.1.100/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full URL with just slash",
|
||||||
|
input: "http://192.168.1.100/",
|
||||||
|
expected: "http://192.168.1.100/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP address only",
|
||||||
|
input: "192.168.1.100",
|
||||||
|
expected: "http://192.168.1.100/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP with port",
|
||||||
|
input: "192.168.1.100:8080",
|
||||||
|
expected: "http://192.168.1.100:8080/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP with default HTTP port",
|
||||||
|
input: "192.168.1.100:80",
|
||||||
|
expected: "http://192.168.1.100:80/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname only",
|
||||||
|
input: "camera.local",
|
||||||
|
expected: "http://camera.local/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname with port",
|
||||||
|
input: "camera.local:8080",
|
||||||
|
expected: "http://camera.local:8080/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS URL",
|
||||||
|
input: "https://192.168.1.100/onvif/device_service",
|
||||||
|
expected: "https://192.168.1.100/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS with custom port",
|
||||||
|
input: "https://192.168.1.100:8443/onvif/device_service",
|
||||||
|
expected: "https://192.168.1.100:8443/onvif/device_service",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with custom path",
|
||||||
|
input: "http://192.168.1.100/custom/path",
|
||||||
|
expected: "http://192.168.1.100/custom/path",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := normalizeEndpoint(tt.input)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("normalizeEndpoint() expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("normalizeEndpoint() unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("normalizeEndpoint() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientWithVariousEndpoints(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
expectScheme string
|
||||||
|
expectHost string
|
||||||
|
expectPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IP only",
|
||||||
|
endpoint: "192.168.1.100",
|
||||||
|
expectScheme: "http",
|
||||||
|
expectHost: "192.168.1.100",
|
||||||
|
expectPath: "/onvif/device_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP with port",
|
||||||
|
endpoint: "192.168.1.100:8080",
|
||||||
|
expectScheme: "http",
|
||||||
|
expectHost: "192.168.1.100:8080",
|
||||||
|
expectPath: "/onvif/device_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Full URL",
|
||||||
|
endpoint: "http://192.168.1.100/onvif/device_service",
|
||||||
|
expectScheme: "http",
|
||||||
|
expectHost: "192.168.1.100",
|
||||||
|
expectPath: "/onvif/device_service",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client, err := NewClient(tt.endpoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewClient() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(client.endpoint, tt.expectScheme+"://") {
|
||||||
|
t.Errorf("Expected scheme %s, got endpoint %s", tt.expectScheme, client.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(client.endpoint, tt.expectHost) {
|
||||||
|
t.Errorf("Expected host %s in endpoint %s", tt.expectHost, client.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(client.endpoint, tt.expectPath) {
|
||||||
|
t.Errorf("Expected path %s in endpoint %s", tt.expectPath, client.endpoint)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mock ONVIF server for comprehensive testing
|
// Mock ONVIF server for comprehensive testing
|
||||||
type MockONVIFServer struct {
|
type MockONVIFServer struct {
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
@@ -482,3 +640,157 @@ func ExampleClient_GetDeviceInformation() {
|
|||||||
fmt.Printf("Camera: %s %s\n", info.Manufacturer, info.Model)
|
fmt.Printf("Camera: %s %s\n", info.Manufacturer, info.Model)
|
||||||
fmt.Printf("Firmware: %s\n", info.FirmwareVersion)
|
fmt.Printf("Firmware: %s\n", info.FirmwareVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFixLocalhostURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clientURL string
|
||||||
|
serviceURL string
|
||||||
|
expectedURL string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "localhost hostname",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://localhost/onvif/media_service",
|
||||||
|
expectedURL: "http://192.168.1.100/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "127.0.0.1 loopback",
|
||||||
|
clientURL: "http://192.168.1.100:8080/onvif/device_service",
|
||||||
|
serviceURL: "http://127.0.0.1/onvif/ptz_service",
|
||||||
|
expectedURL: "http://192.168.1.100:8080/onvif/ptz_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0.0.0.0 address",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://0.0.0.0/onvif/imaging_service",
|
||||||
|
expectedURL: "http://192.168.1.100/onvif/imaging_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 loopback",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://[::1]/onvif/events_service",
|
||||||
|
expectedURL: "http://192.168.1.100/onvif/events_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "localhost with different port",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://localhost:8080/onvif/media_service",
|
||||||
|
expectedURL: "http://192.168.1.100:8080/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid IP address unchanged",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://192.168.1.100/onvif/media_service",
|
||||||
|
expectedURL: "http://192.168.1.100/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different valid IP unchanged",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "http://192.168.1.50/onvif/media_service",
|
||||||
|
expectedURL: "http://192.168.1.50/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS localhost",
|
||||||
|
clientURL: "https://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "https://localhost/onvif/media_service",
|
||||||
|
expectedURL: "https://192.168.1.100/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "client with port, service localhost no port",
|
||||||
|
clientURL: "http://192.168.1.100:80/onvif/device_service",
|
||||||
|
serviceURL: "http://localhost/onvif/media_service",
|
||||||
|
expectedURL: "http://192.168.1.100:80/onvif/media_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty service URL",
|
||||||
|
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||||
|
serviceURL: "",
|
||||||
|
expectedURL: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client := &Client{
|
||||||
|
endpoint: tt.clientURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := client.fixLocalhostURL(tt.serviceURL)
|
||||||
|
if result != tt.expectedURL {
|
||||||
|
t.Errorf("fixLocalhostURL() = %v, want %v", result, tt.expectedURL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeWithLocalhostURLs(t *testing.T) {
|
||||||
|
// Create a mock server
|
||||||
|
mock := NewMockONVIFServer()
|
||||||
|
defer mock.Close()
|
||||||
|
|
||||||
|
// Set a GetCapabilities response with localhost URLs
|
||||||
|
capabilitiesResponse := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||||
|
<SOAP-ENV:Body>
|
||||||
|
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||||
|
<tds:Capabilities>
|
||||||
|
<tt:Media xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||||
|
<tt:XAddr>http://localhost:8080/onvif/media_service</tt:XAddr>
|
||||||
|
</tt:Media>
|
||||||
|
<tt:PTZ xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||||
|
<tt:XAddr>http://127.0.0.1/onvif/ptz_service</tt:XAddr>
|
||||||
|
</tt:PTZ>
|
||||||
|
<tt:Imaging xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||||
|
<tt:XAddr>http://0.0.0.0/onvif/imaging_service</tt:XAddr>
|
||||||
|
</tt:Imaging>
|
||||||
|
</tds:Capabilities>
|
||||||
|
</tds:GetCapabilitiesResponse>
|
||||||
|
</SOAP-ENV:Body>
|
||||||
|
</SOAP-ENV:Envelope>`
|
||||||
|
|
||||||
|
mock.SetResponse("GetCapabilities", capabilitiesResponse)
|
||||||
|
|
||||||
|
// Create client pointing to mock server
|
||||||
|
client, err := NewClient(
|
||||||
|
mock.URL()+"/onvif/device_service",
|
||||||
|
WithCredentials("admin", "admin"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize should fix localhost URLs
|
||||||
|
ctx := context.Background()
|
||||||
|
err = client.Initialize(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initialize() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the mock server URL to get host
|
||||||
|
mockURL, _ := url.Parse(mock.URL())
|
||||||
|
expectedHost := mockURL.Host
|
||||||
|
|
||||||
|
// Verify media endpoint was fixed (localhost:8080 should be replaced with mock host)
|
||||||
|
if strings.Contains(client.mediaEndpoint, "localhost") {
|
||||||
|
t.Errorf("Media endpoint still contains localhost: %v", client.mediaEndpoint)
|
||||||
|
}
|
||||||
|
if !strings.Contains(client.mediaEndpoint, expectedHost) {
|
||||||
|
t.Logf("Media endpoint: %v, Expected to contain: %v", client.mediaEndpoint, expectedHost)
|
||||||
|
// The port 8080 from service URL should be preserved
|
||||||
|
expectedMediaURL := "http://" + mockURL.Hostname() + ":8080/onvif/media_service"
|
||||||
|
if client.mediaEndpoint != expectedMediaURL {
|
||||||
|
t.Errorf("Media endpoint = %v, want %v", client.mediaEndpoint, expectedMediaURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PTZ endpoint was fixed (127.0.0.1 should be replaced with mock host)
|
||||||
|
if strings.Contains(client.ptzEndpoint, "127.0.0.1") && !strings.Contains(expectedHost, "127.0.0.1") {
|
||||||
|
t.Errorf("PTZ endpoint still contains 127.0.0.1: %v", client.ptzEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Imaging endpoint was fixed (0.0.0.0 should be replaced with mock host)
|
||||||
|
if strings.Contains(client.imagingEndpoint, "0.0.0.0") {
|
||||||
|
t.Errorf("Imaging endpoint still contains 0.0.0.0: %v", client.imagingEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Test Generator
|
||||||
|
|
||||||
|
Automatically generate Go tests from captured ONVIF camera XML traffic.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool reads XML capture archives (created by `onvif-diagnostics -capture-xml`) and generates complete Go test files that replay the captured SOAP traffic through a mock server.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Camera_Model_xmlcapture_timestamp.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-capture string
|
||||||
|
Path to XML capture archive (.tar.gz) (required)
|
||||||
|
|
||||||
|
-output string
|
||||||
|
Output directory for generated test file (default: "./")
|
||||||
|
|
||||||
|
-package string
|
||||||
|
Package name for generated test (default: "onvif_test")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate test from Bosch camera capture
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_xmlcapture_20251110-120000.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# ✓ Generated test file: testdata/captures/bosch_flexidome_indoor_5100i_ir_8.71.0066_test.go
|
||||||
|
# Camera: Bosch FLEXIDOME indoor 5100i IR (Firmware: 8.71.0066)
|
||||||
|
# Captured operations: 18
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated Test Structure
|
||||||
|
|
||||||
|
The tool creates a complete test file with:
|
||||||
|
|
||||||
|
### Test Function
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Test<CameraName>(t *testing.T)
|
||||||
|
```
|
||||||
|
|
||||||
|
Named based on camera manufacturer, model, and firmware.
|
||||||
|
|
||||||
|
### Subtests
|
||||||
|
|
||||||
|
- `GetDeviceInformation` - Validates device info parsing
|
||||||
|
- `GetSystemDateAndTime` - Tests date/time operation
|
||||||
|
- `GetCapabilities` - Verifies capability discovery
|
||||||
|
- `GetProfiles` - Tests media profile enumeration
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
Each subtest includes:
|
||||||
|
- Error checking
|
||||||
|
- Nil validation
|
||||||
|
- Basic field validation
|
||||||
|
- Informative logging
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Load Capture** - Reads all SOAP exchanges from tar.gz archive
|
||||||
|
2. **Extract Metadata** - Gets camera manufacturer, model, firmware from responses
|
||||||
|
3. **Generate Name** - Creates valid Go identifier from camera info
|
||||||
|
4. **Render Template** - Fills in test template with camera-specific data
|
||||||
|
5. **Write File** - Saves test to output directory
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
The generator uses an embedded Go template that creates:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package onvif_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0x524a/onvif-go"
|
||||||
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test<CameraName>(t *testing.T) {
|
||||||
|
captureArchive := "<archive-file>.tar.gz"
|
||||||
|
|
||||||
|
mockServer, err := onviftesting.NewMockSOAPServer(captureArchive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create mock server: %v", err)
|
||||||
|
}
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
client, err := onvif.NewClient(
|
||||||
|
mockServer.URL()+"/onvif/device_service",
|
||||||
|
onvif.WithCredentials("testuser", "testpass"),
|
||||||
|
)
|
||||||
|
// ... test operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Capture from Camera
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://camera/onvif/device_service" \
|
||||||
|
-username "user" \
|
||||||
|
-password "pass" \
|
||||||
|
-capture-xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-tests \
|
||||||
|
-capture camera-logs/Camera_*_xmlcapture_*.tar.gz \
|
||||||
|
-output testdata/captures/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -v ./testdata/captures/ -run TestCamera
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
After generation, you can customize the test:
|
||||||
|
|
||||||
|
### Add Camera-Specific Tests
|
||||||
|
|
||||||
|
```go
|
||||||
|
t.Run("CustomFeature", func(t *testing.T) {
|
||||||
|
// Add custom test for camera-specific features
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Detailed Assertions
|
||||||
|
|
||||||
|
```go
|
||||||
|
t.Run("GetDeviceInformation", func(t *testing.T) {
|
||||||
|
info, err := client.GetDeviceInformation(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetDeviceInformation failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add specific assertions
|
||||||
|
if info.Manufacturer != "ExpectedManufacturer" {
|
||||||
|
t.Errorf("Expected manufacturer X, got %s", info.Manufacturer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o generate-tests ./cmd/generate-tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `github.com/0x524a/onvif-go/testing` - Mock server and capture loader
|
||||||
|
|
||||||
|
## Output File Naming
|
||||||
|
|
||||||
|
Generated test files are named:
|
||||||
|
|
||||||
|
```
|
||||||
|
<manufacturer>_<model>_<firmware>_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `bosch_flexidome_indoor_5100i_ir_8.71.0066_test.go`
|
||||||
|
- `axis_q3626-ve_12.6.104_test.go`
|
||||||
|
- `reolink_e1_zoom_v3.1.0.2649_test.go`
|
||||||
|
|
||||||
|
All special characters converted to underscores or removed.
|
||||||
|
|
||||||
|
## Archive Path Handling
|
||||||
|
|
||||||
|
The generator automatically handles archive paths:
|
||||||
|
|
||||||
|
- If archive is in output directory, uses filename only
|
||||||
|
- Otherwise uses relative path from output directory
|
||||||
|
- Tests can find archives when run with `go test ./testdata/captures/`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Failed to load capture"
|
||||||
|
|
||||||
|
Archive file not found or corrupted.
|
||||||
|
|
||||||
|
**Solution**: Verify archive path and ensure it's a valid tar.gz file.
|
||||||
|
|
||||||
|
### "Failed to extract device info"
|
||||||
|
|
||||||
|
Archive doesn't contain GetDeviceInformation response.
|
||||||
|
|
||||||
|
**Solution**: Re-capture from camera, ensuring diagnostic runs fully.
|
||||||
|
|
||||||
|
### Generated test won't compile
|
||||||
|
|
||||||
|
Usually due to invalid characters in camera names.
|
||||||
|
|
||||||
|
**Solution**: The generator should handle this, but you can manually edit the test function name.
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
|
||||||
|
- [ ] Detect camera-specific operations (PTZ, audio, etc.)
|
||||||
|
- [ ] Generate profile-specific tests
|
||||||
|
- [ ] Add benchmarking subtests
|
||||||
|
- [ ] Support custom test templates
|
||||||
|
- [ ] Batch generation from multiple captures
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- `testdata/captures/README.md` - Using generated tests
|
||||||
|
- `testing/mock_server.go` - Mock server implementation
|
||||||
|
- `cmd/onvif-diagnostics/` - Capturing tool
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
captureArchive = flag.String("capture", "", "Path to XML capture archive (.tar.gz)")
|
||||||
|
outputDir = flag.String("output", "./", "Output directory for generated test file")
|
||||||
|
packageName = flag.String("package", "onvif_test", "Package name for generated test")
|
||||||
|
)
|
||||||
|
|
||||||
|
const testTemplate = `package {{.PackageName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0x524a/onvif-go"
|
||||||
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test{{.CameraName}} tests ONVIF client against {{.CameraDescription}} captured responses
|
||||||
|
func Test{{.CameraName}}(t *testing.T) {
|
||||||
|
// Load capture archive (relative to project root)
|
||||||
|
captureArchive := "{{.CaptureArchiveRelPath}}"
|
||||||
|
|
||||||
|
mockServer, err := onviftesting.NewMockSOAPServer(captureArchive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create mock server: %v", err)
|
||||||
|
}
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
// Create ONVIF client pointing to mock server
|
||||||
|
client, err := onvif.NewClient(
|
||||||
|
mockServer.URL()+"/onvif/device_service",
|
||||||
|
onvif.WithCredentials("testuser", "testpass"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create ONVIF client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
t.Run("GetDeviceInformation", func(t *testing.T) {
|
||||||
|
info, err := client.GetDeviceInformation(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetDeviceInformation failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate expected values
|
||||||
|
if info.Manufacturer == "" {
|
||||||
|
t.Error("Manufacturer is empty")
|
||||||
|
}
|
||||||
|
if info.Model == "" {
|
||||||
|
t.Error("Model is empty")
|
||||||
|
}
|
||||||
|
if info.FirmwareVersion == "" {
|
||||||
|
t.Error("FirmwareVersion is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Device: %s %s (Firmware: %s)", info.Manufacturer, info.Model, info.FirmwareVersion)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetSystemDateAndTime", func(t *testing.T) {
|
||||||
|
_, err := client.GetSystemDateAndTime(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetSystemDateAndTime failed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetCapabilities", func(t *testing.T) {
|
||||||
|
caps, err := client.GetCapabilities(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetCapabilities failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if caps.Device == nil {
|
||||||
|
t.Error("Device capabilities is nil")
|
||||||
|
}
|
||||||
|
if caps.Media == nil {
|
||||||
|
t.Error("Media capabilities is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Capabilities: Device=%v, Media=%v, Imaging=%v, PTZ=%v",
|
||||||
|
caps.Device != nil, caps.Media != nil, caps.Imaging != nil, caps.PTZ != nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetProfiles", func(t *testing.T) {
|
||||||
|
profiles, err := client.GetProfiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetProfiles failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(profiles) == 0 {
|
||||||
|
t.Error("No profiles returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Found %d profile(s)", len(profiles))
|
||||||
|
for i, profile := range profiles {
|
||||||
|
t.Logf(" Profile %d: %s (Token: %s)", i+1, profile.Name, profile.Token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{{range .AdditionalTests}}
|
||||||
|
t.Run("{{.Name}}", func(t *testing.T) {
|
||||||
|
{{.Code}}
|
||||||
|
})
|
||||||
|
{{end}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type TestData struct {
|
||||||
|
PackageName string
|
||||||
|
CameraName string
|
||||||
|
CameraDescription string
|
||||||
|
CaptureArchiveRelPath string
|
||||||
|
AdditionalTests []AdditionalTest
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalTest struct {
|
||||||
|
Name string
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *captureArchive == "" {
|
||||||
|
fmt.Println("Error: -capture flag is required")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Example:")
|
||||||
|
fmt.Println(" ./generate-tests -capture camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_xmlcapture_*.tar.gz")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load capture to get camera info
|
||||||
|
capture, err := onviftesting.LoadCaptureFromArchive(*captureArchive)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load capture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract camera name from archive filename
|
||||||
|
baseName := filepath.Base(*captureArchive)
|
||||||
|
// Remove _xmlcapture_timestamp.tar.gz suffix
|
||||||
|
parts := strings.Split(baseName, "_xmlcapture_")
|
||||||
|
cameraID := parts[0]
|
||||||
|
|
||||||
|
// Convert to valid Go identifier
|
||||||
|
cameraName := strings.ReplaceAll(cameraID, "-", "")
|
||||||
|
cameraName = strings.ReplaceAll(cameraName, ".", "")
|
||||||
|
cameraName = strings.ReplaceAll(cameraName, " ", "")
|
||||||
|
|
||||||
|
// Get device info from first exchange (GetDeviceInformation)
|
||||||
|
cameraDesc := cameraID
|
||||||
|
if len(capture.Exchanges) > 0 {
|
||||||
|
// Try to parse device info from response
|
||||||
|
for _, ex := range capture.Exchanges {
|
||||||
|
if strings.Contains(ex.RequestBody, "GetDeviceInformation") {
|
||||||
|
// Extract manufacturer and model from response
|
||||||
|
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
||||||
|
model := extractXMLValue(ex.ResponseBody, "Model")
|
||||||
|
firmware := extractXMLValue(ex.ResponseBody, "FirmwareVersion")
|
||||||
|
if manufacturer != "" && model != "" {
|
||||||
|
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare test data
|
||||||
|
// Make archive path relative if inside output directory
|
||||||
|
relArchivePath := *captureArchive
|
||||||
|
|
||||||
|
// If archive is in a sibling directory to output, make it relative
|
||||||
|
if absOutput, err := filepath.Abs(*outputDir); err == nil {
|
||||||
|
if absArchive, err := filepath.Abs(*captureArchive); err == nil {
|
||||||
|
if rel, err := filepath.Rel(filepath.Dir(absOutput), absArchive); err == nil {
|
||||||
|
relArchivePath = rel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testData := TestData{
|
||||||
|
PackageName: *packageName,
|
||||||
|
CameraName: cameraName,
|
||||||
|
CameraDescription: cameraDesc,
|
||||||
|
CaptureArchiveRelPath: relArchivePath,
|
||||||
|
AdditionalTests: []AdditionalTest{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate test file
|
||||||
|
tmpl, err := template.New("test").Parse(testTemplate)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output file
|
||||||
|
outputFile := filepath.Join(*outputDir, fmt.Sprintf("%s_test.go", strings.ToLower(cameraID)))
|
||||||
|
f, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := tmpl.Execute(f, testData); err != nil {
|
||||||
|
log.Fatalf("Failed to execute template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ Generated test file: %s\n", outputFile)
|
||||||
|
fmt.Printf(" Camera: %s\n", cameraDesc)
|
||||||
|
fmt.Printf(" Captured operations: %d\n", len(capture.Exchanges))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Run tests with:")
|
||||||
|
fmt.Printf(" go test -v %s\n", outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractXMLValue(xmlStr, tagName string) string {
|
||||||
|
// Simple extraction for basic tags
|
||||||
|
start := fmt.Sprintf("<%s>", tagName)
|
||||||
|
end := fmt.Sprintf("</%s>", tagName)
|
||||||
|
|
||||||
|
startIdx := strings.Index(xmlStr, start)
|
||||||
|
if startIdx == -1 {
|
||||||
|
// Try with namespace prefix
|
||||||
|
start = fmt.Sprintf(":%s>", tagName)
|
||||||
|
startIdx = strings.Index(xmlStr, start)
|
||||||
|
if startIdx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
startIdx += len(start)
|
||||||
|
} else {
|
||||||
|
startIdx += len(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx := strings.Index(xmlStr[startIdx:], end)
|
||||||
|
if endIdx == -1 {
|
||||||
|
// Try with namespace prefix
|
||||||
|
end = fmt.Sprintf(":/%s>", tagName)
|
||||||
|
endIdx = strings.Index(xmlStr[startIdx:], end)
|
||||||
|
if endIdx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(xmlStr[startIdx : startIdx+endIdx])
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# ONVIF Camera Diagnostic Utility
|
||||||
|
|
||||||
|
A comprehensive diagnostic tool for collecting detailed information from ONVIF cameras. This utility helps analyze camera capabilities, troubleshoot issues, and generate reports for creating camera-specific tests.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✅ **Comprehensive Testing** - Tests all major ONVIF operations:
|
||||||
|
- Device information and capabilities
|
||||||
|
- Media profiles and streaming
|
||||||
|
- Video encoder configurations
|
||||||
|
- Imaging settings
|
||||||
|
- PTZ status and presets (if available)
|
||||||
|
- System date/time
|
||||||
|
|
||||||
|
✅ **Detailed Reporting** - Generates JSON reports with:
|
||||||
|
- All successful operations with response data
|
||||||
|
- Failed operations with error details
|
||||||
|
- Response times for performance analysis
|
||||||
|
- Structured data ready for test generation
|
||||||
|
|
||||||
|
✅ **Easy to Use** - Simple command-line interface with minimal requirements
|
||||||
|
|
||||||
|
✅ **XML Debugging** - For detailed debugging, see the companion `onvif-xml-capture` utility that captures raw SOAP XML
|
||||||
|
|
||||||
|
✅ **Helpful for**:
|
||||||
|
- Creating camera-specific integration tests
|
||||||
|
- Troubleshooting ONVIF compatibility issues
|
||||||
|
- Analyzing camera capabilities
|
||||||
|
- Debugging connection problems
|
||||||
|
- Documenting camera configurations
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Build from source
|
||||||
|
```bash
|
||||||
|
cd /path/to/go-onvif
|
||||||
|
go build -o onvif-diagnostics ./cmd/onvif-diagnostics/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Install globally
|
||||||
|
```bash
|
||||||
|
go install ./cmd/onvif-diagnostics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
### With XML Capture (for debugging)
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-capture-xml \
|
||||||
|
-verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates two files:
|
||||||
|
- `Manufacturer_Model_Firmware_timestamp.json` - Diagnostic report
|
||||||
|
- `Manufacturer_Model_Firmware_xmlcapture_timestamp.tar.gz` - Raw SOAP XML archive
|
||||||
|
|
||||||
|
### Verbose Output
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capture Raw SOAP XML
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-capture-xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Enables XML traffic capture and creates a compressed tar.gz archive containing all SOAP request/response pairs. Useful for debugging XML parsing issues or analyzing camera behavior.
|
||||||
|
|
||||||
|
The archive contains:
|
||||||
|
- `capture_001_GetDeviceInformation.json` - Request/response metadata with operation name
|
||||||
|
- `capture_001_GetDeviceInformation_request.xml` - Formatted SOAP request
|
||||||
|
- `capture_001_GetDeviceInformation_response.xml` - Formatted SOAP response
|
||||||
|
- `capture_002_GetSystemDateAndTime.json` - Next operation metadata
|
||||||
|
- ... (one set per SOAP operation, named by operation type)
|
||||||
|
|
||||||
|
Each file is named with the SOAP operation (e.g., GetDeviceInformation, GetProfiles) for easy identification.
|
||||||
|
|
||||||
|
Extract the archive:
|
||||||
|
```bash
|
||||||
|
tar -xzf camera-logs/Camera_Model_xmlcapture_timestamp.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Output Directory
|
||||||
|
```bash
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://192.168.1.201/onvif/device_service" \
|
||||||
|
-username "service" \
|
||||||
|
-password "Service.1234" \
|
||||||
|
-output ./my-camera-reports
|
||||||
|
```
|
||||||
|
|
||||||
|
### All Options
|
||||||
|
```
|
||||||
|
Usage of ./onvif-diagnostics:
|
||||||
|
-endpoint string
|
||||||
|
ONVIF device endpoint (e.g., http://192.168.1.201/onvif/device_service)
|
||||||
|
-username string
|
||||||
|
ONVIF username
|
||||||
|
-password string
|
||||||
|
ONVIF password
|
||||||
|
-output string
|
||||||
|
Output directory for logs (default "./camera-logs")
|
||||||
|
-timeout int
|
||||||
|
Request timeout in seconds (default 30)
|
||||||
|
-verbose
|
||||||
|
Verbose output
|
||||||
|
-include-raw
|
||||||
|
Include raw SOAP responses (increases file size)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
ONVIF Camera Diagnostic Utility v1.0.0
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Starting diagnostic collection...
|
||||||
|
|
||||||
|
→ 1. Getting device information...
|
||||||
|
✓ Manufacturer: Bosch, Model: FLEXIDOME indoor 5100i IR
|
||||||
|
→ 2. Getting system date and time...
|
||||||
|
✓ Retrieved
|
||||||
|
→ 3. Getting capabilities...
|
||||||
|
✓ Services: Device, Media, Imaging, Events, Analytics
|
||||||
|
→ 4. Discovering service endpoints...
|
||||||
|
✓ Service endpoints discovered
|
||||||
|
→ 5. Getting media profiles...
|
||||||
|
✓ Found 4 profile(s)
|
||||||
|
→ 6. Getting stream URIs for all profiles...
|
||||||
|
✓ Retrieved 4/4 stream URIs
|
||||||
|
→ 7. Getting snapshot URIs for all profiles...
|
||||||
|
✓ Retrieved 4/4 snapshot URIs
|
||||||
|
→ 8. Getting video encoder configurations...
|
||||||
|
✓ Retrieved 4/4 video encoder configs
|
||||||
|
→ 9. Getting imaging settings...
|
||||||
|
✓ Retrieved 1/1 imaging settings
|
||||||
|
→ 10. Getting PTZ status...
|
||||||
|
ℹ No PTZ configurations found
|
||||||
|
→ 11. Getting PTZ presets...
|
||||||
|
ℹ No PTZ configurations found
|
||||||
|
→ Saving diagnostic report...
|
||||||
|
|
||||||
|
========================================
|
||||||
|
✓ Diagnostic collection complete!
|
||||||
|
Report saved to: camera-logs/Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066_20251107-193656.json
|
||||||
|
Total errors: 0
|
||||||
|
|
||||||
|
Device: Bosch FLEXIDOME indoor 5100i IR
|
||||||
|
Firmware: 8.71.0066
|
||||||
|
Profiles: 4
|
||||||
|
|
||||||
|
Please share this file for analysis and test creation.
|
||||||
|
========================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report Structure
|
||||||
|
|
||||||
|
The generated JSON report includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-07T19:36:56Z",
|
||||||
|
"utility_version": "1.0.0",
|
||||||
|
"connection_info": {
|
||||||
|
"endpoint": "http://192.168.1.201/onvif/device_service",
|
||||||
|
"username": "service",
|
||||||
|
"test_date": "2025-11-07"
|
||||||
|
},
|
||||||
|
"device_info": {
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"manufacturer": "Bosch",
|
||||||
|
"model": "FLEXIDOME indoor 5100i IR",
|
||||||
|
"firmware_version": "8.71.0066",
|
||||||
|
"serial_number": "404754734001050102",
|
||||||
|
"hardware_id": "F000B543"
|
||||||
|
},
|
||||||
|
"response_time": "21.5ms"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"success": true,
|
||||||
|
"count": 4,
|
||||||
|
"data": [ /* profile details */ ]
|
||||||
|
},
|
||||||
|
"stream_uris": [ /* stream URI results for each profile */ ],
|
||||||
|
"errors": [ /* any errors encountered */ ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### 1. Creating Camera-Specific Tests
|
||||||
|
Run the diagnostic on your camera and share the JSON file. The report contains all the information needed to create comprehensive integration tests.
|
||||||
|
|
||||||
|
### 2. Troubleshooting Connection Issues
|
||||||
|
If your camera isn't working, run diagnostics to see exactly which operations fail and what error messages are returned.
|
||||||
|
|
||||||
|
### 3. Comparing Cameras
|
||||||
|
Run diagnostics on multiple cameras to compare capabilities, response times, and compatibility.
|
||||||
|
|
||||||
|
### 4. Documentation
|
||||||
|
Generate detailed reports of camera configurations for documentation purposes.
|
||||||
|
|
||||||
|
## Interpreting Results
|
||||||
|
|
||||||
|
### Success Indicators
|
||||||
|
- ✓ Green checkmarks indicate successful operations
|
||||||
|
- Response times help identify performance issues
|
||||||
|
- High success rates indicate good compatibility
|
||||||
|
|
||||||
|
### Error Indicators
|
||||||
|
- ✗ Red X marks indicate failed operations
|
||||||
|
- ℹ Info symbols indicate optional features not available
|
||||||
|
- Check the `errors` array in JSON for detailed error messages
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**All operations fail:**
|
||||||
|
- Check network connectivity
|
||||||
|
- Verify endpoint URL is correct
|
||||||
|
- Ensure camera is powered on
|
||||||
|
|
||||||
|
**Authentication errors:**
|
||||||
|
- Verify username and password
|
||||||
|
- Check user permissions on camera
|
||||||
|
|
||||||
|
**Some profiles fail:**
|
||||||
|
- Camera may have different capabilities per profile
|
||||||
|
- Some operations may not be supported by all profiles
|
||||||
|
|
||||||
|
**Timeout errors:**
|
||||||
|
- Increase timeout with `-timeout 60`
|
||||||
|
- Check network latency
|
||||||
|
- Verify camera is responding
|
||||||
|
|
||||||
|
## Sharing Reports
|
||||||
|
|
||||||
|
When sharing diagnostic reports:
|
||||||
|
|
||||||
|
1. **Anonymize if needed** - The report includes:
|
||||||
|
- IP addresses (in endpoint)
|
||||||
|
- Usernames (not passwords)
|
||||||
|
- Serial numbers
|
||||||
|
|
||||||
|
2. **What to share**:
|
||||||
|
- The complete JSON file
|
||||||
|
- Any console output showing errors
|
||||||
|
- Camera model and firmware version
|
||||||
|
|
||||||
|
3. **Where to share**:
|
||||||
|
- GitHub Issues
|
||||||
|
- Email for analysis
|
||||||
|
- Pull request descriptions
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Batch Testing Multiple Cameras
|
||||||
|
Create a script to test multiple cameras:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
cameras=(
|
||||||
|
"192.168.1.201:service:password1"
|
||||||
|
"192.168.1.202:admin:password2"
|
||||||
|
"192.168.1.203:user:password3"
|
||||||
|
)
|
||||||
|
|
||||||
|
for camera in "${cameras[@]}"; do
|
||||||
|
IFS=':' read -r ip user pass <<< "$camera"
|
||||||
|
echo "Testing camera at $ip..."
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "http://$ip/onvif/device_service" \
|
||||||
|
-username "$user" \
|
||||||
|
-password "$pass"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
Include in CI/CD pipelines:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Run ONVIF Diagnostics
|
||||||
|
run: |
|
||||||
|
./onvif-diagnostics \
|
||||||
|
-endpoint "${{ secrets.CAMERA_ENDPOINT }}" \
|
||||||
|
-username "${{ secrets.CAMERA_USERNAME }}" \
|
||||||
|
-password "${{ secrets.CAMERA_PASSWORD }}" \
|
||||||
|
-output ./reports
|
||||||
|
|
||||||
|
- name: Upload Diagnostic Reports
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: camera-diagnostics
|
||||||
|
path: ./reports/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New Tests
|
||||||
|
|
||||||
|
To add new diagnostic tests, edit `cmd/onvif-diagnostics/main.go`:
|
||||||
|
|
||||||
|
1. Create a new test function following the pattern:
|
||||||
|
```go
|
||||||
|
func testNewOperation(ctx context.Context, client *onvif.Client, report *CameraReport) *NewOperationResult {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add result struct to store data
|
||||||
|
3. Call the test in main()
|
||||||
|
4. Update report structure
|
||||||
|
|
||||||
|
### Building for Different Platforms
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o onvif-diagnostics-linux ./cmd/onvif-diagnostics/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o onvif-diagnostics.exe ./cmd/onvif-diagnostics/
|
||||||
|
|
||||||
|
# macOS ARM
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -o onvif-diagnostics-mac-arm ./cmd/onvif-diagnostics/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as parent project.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Run diagnostics with `-verbose` flag
|
||||||
|
2. Share the generated JSON report
|
||||||
|
3. **For XML parsing issues**: Use `onvif-xml-capture` utility to capture raw SOAP XML
|
||||||
|
4. Open a GitHub issue with the report attached
|
||||||
|
|
||||||
|
## Related Tools
|
||||||
|
|
||||||
|
- **onvif-xml-capture** - Captures raw SOAP XML requests/responses for detailed debugging
|
||||||
|
- Location: `cmd/onvif-xml-capture/`
|
||||||
|
- Use when: Diagnostic report shows errors and you need to see raw XML
|
||||||
|
- See: `XML_DEBUGGING_SOLUTION.md` for complete guide
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/soap"
|
"github.com/0x524a/onvif-go/internal/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Device service namespace
|
// Device service namespace
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ go-onvif is a modern, performant Go library for communicating with ONVIF-complia
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
The project follows the **Standard Go Project Layout** for libraries:
|
||||||
|
|
||||||
|
```
|
||||||
|
onvif-go/
|
||||||
|
├── *.go # Public API (client.go, device.go, media.go, ptz.go, imaging.go)
|
||||||
|
├── internal/ # Private implementation details
|
||||||
|
│ └── soap/ # SOAP client (not exported)
|
||||||
|
├── discovery/ # Device discovery (public subpackage)
|
||||||
|
├── server/ # ONVIF server implementation (public subpackage)
|
||||||
|
├── cmd/ # Command-line tools
|
||||||
|
├── examples/ # Usage examples
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── testing/ # Testing helpers
|
||||||
|
└── testdata/ # Test fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
**Design Rationale:**
|
||||||
|
- **Root-level API**: Main package at root for clean imports (`github.com/0x524a/onvif-go`)
|
||||||
|
- **internal/**: Private packages not intended for external use (SOAP implementation)
|
||||||
|
- **Subpackages**: Additional features like `discovery/` and `server/`
|
||||||
|
- **cmd/**: Executable applications and tools
|
||||||
|
- **examples/**: Demonstrate library usage
|
||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -27,7 +52,7 @@ go-onvif is a modern, performant Go library for communicating with ONVIF-complia
|
|||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ Transport Layer │
|
│ Transport Layer │
|
||||||
│ - SOAP Client (soap/soap.go) │
|
│ - SOAP Client (internal/soap/soap.go) │
|
||||||
│ - WS-Security Authentication │
|
│ - WS-Security Authentication │
|
||||||
│ - XML Marshaling/Unmarshaling │
|
│ - XML Marshaling/Unmarshaling │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
@@ -85,7 +85,7 @@ We have successfully created a **comprehensive, production-ready Go ONVIF librar
|
|||||||
|
|
||||||
### Basic Library Usage
|
### Basic Library Usage
|
||||||
```go
|
```go
|
||||||
import "github.com/0x524A/go-onvif"
|
import "github.com/0x524a/onvif-go"
|
||||||
|
|
||||||
client, err := onvif.NewClient(
|
client, err := onvif.NewClient(
|
||||||
"http://192.168.1.100/onvif/device_service",
|
"http://192.168.1.100/onvif/device_service",
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
# Project Structure
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `onvif-go` project follows the **Standard Go Project Layout** optimized for a library package. This structure provides clear separation between public APIs, private implementation details, executable commands, and supporting resources.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
onvif-go/
|
||||||
|
├── *.go # Public API files (root level)
|
||||||
|
│ ├── client.go # Main ONVIF client
|
||||||
|
│ ├── device.go # Device service operations
|
||||||
|
│ ├── media.go # Media service operations
|
||||||
|
│ ├── ptz.go # PTZ service operations
|
||||||
|
│ ├── imaging.go # Imaging service operations
|
||||||
|
│ ├── types.go # Public type definitions
|
||||||
|
│ ├── errors.go # Error types and handling
|
||||||
|
│ └── doc.go # Package documentation
|
||||||
|
│
|
||||||
|
├── internal/ # Private packages (not importable externally)
|
||||||
|
│ └── soap/ # SOAP client implementation
|
||||||
|
│ ├── soap.go # SOAP envelope building and parsing
|
||||||
|
│ └── soap_test.go # SOAP client tests
|
||||||
|
│
|
||||||
|
├── discovery/ # Device discovery subpackage (public)
|
||||||
|
│ ├── discovery.go # WS-Discovery implementation
|
||||||
|
│ └── discovery_test.go # Discovery tests
|
||||||
|
│
|
||||||
|
├── server/ # ONVIF server implementation (public)
|
||||||
|
│ ├── server.go # Main server
|
||||||
|
│ ├── device.go # Device service handlers
|
||||||
|
│ ├── media.go # Media service handlers
|
||||||
|
│ ├── ptz.go # PTZ service handlers
|
||||||
|
│ ├── imaging.go # Imaging service handlers
|
||||||
|
│ └── soap/ # Server SOAP handling
|
||||||
|
│ └── handler.go # SOAP request handler
|
||||||
|
│
|
||||||
|
├── cmd/ # Command-line applications
|
||||||
|
│ ├── onvif-cli/ # Interactive CLI tool
|
||||||
|
│ ├── onvif-quick/ # Quick test utility
|
||||||
|
│ ├── onvif-server/ # Virtual camera server
|
||||||
|
│ ├── onvif-diagnostics/ # Diagnostic tool
|
||||||
|
│ └── generate-tests/ # Test generation utility
|
||||||
|
│
|
||||||
|
├── examples/ # Example applications
|
||||||
|
│ ├── device-info/ # Get device information
|
||||||
|
│ ├── discovery/ # Discover cameras
|
||||||
|
│ ├── ptz-control/ # PTZ operations
|
||||||
|
│ ├── imaging-settings/ # Imaging configuration
|
||||||
|
│ ├── complete-demo/ # Full feature demo
|
||||||
|
│ ├── simplified-endpoint/ # Endpoint format demo
|
||||||
|
│ ├── test-server/ # Server testing example
|
||||||
|
│ └── .../ # Additional examples
|
||||||
|
│
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── ARCHITECTURE.md # Architecture overview
|
||||||
|
│ ├── PROJECT_STRUCTURE.md # This file
|
||||||
|
│ ├── SIMPLIFIED_ENDPOINT.md # Endpoint API docs
|
||||||
|
│ └── .../ # Additional documentation
|
||||||
|
│
|
||||||
|
├── testdata/ # Test fixtures and data
|
||||||
|
├── testing/ # Testing helpers
|
||||||
|
│
|
||||||
|
├── .github/ # GitHub workflows and configs
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── release.yml # Release automation
|
||||||
|
│
|
||||||
|
├── go.mod # Go module definition
|
||||||
|
├── go.sum # Dependency checksums
|
||||||
|
├── Makefile # Build automation
|
||||||
|
├── Dockerfile # Container image
|
||||||
|
├── README.md # Project readme
|
||||||
|
├── CHANGELOG.md # Version history
|
||||||
|
├── LICENSE # License information
|
||||||
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
├── QUICKSTART.md # Quick start guide
|
||||||
|
└── BUILDING.md # Build instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
### 1. Library-First Design
|
||||||
|
|
||||||
|
As a **library package**, the main API lives at the root level:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/0x524a/onvif-go"
|
||||||
|
|
||||||
|
client, err := onvif.NewClient("192.168.1.100")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Clean, simple import path
|
||||||
|
- Follows Go conventions for libraries
|
||||||
|
- Easy to discover and use
|
||||||
|
- No unnecessary nesting
|
||||||
|
|
||||||
|
### 2. Internal Package for Private Code
|
||||||
|
|
||||||
|
The `internal/` directory contains implementation details not intended for external use:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// This import is ONLY available within onvif-go:
|
||||||
|
import "github.com/0x524a/onvif-go/internal/soap"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Go's internal package restriction:**
|
||||||
|
- Cannot be imported by external projects
|
||||||
|
- Enforced by the Go compiler
|
||||||
|
- Allows refactoring without breaking changes
|
||||||
|
|
||||||
|
**What goes in internal/**:
|
||||||
|
- SOAP client implementation
|
||||||
|
- Protocol-specific details
|
||||||
|
- Helper functions not part of public API
|
||||||
|
- Implementation details that might change
|
||||||
|
|
||||||
|
### 3. Subpackages for Additional Features
|
||||||
|
|
||||||
|
Public subpackages for optional or specialized functionality:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Discovery subpackage
|
||||||
|
import "github.com/0x524a/onvif-go/discovery"
|
||||||
|
|
||||||
|
// Server subpackage
|
||||||
|
import "github.com/0x524a/onvif-go/server"
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to create a subpackage:**
|
||||||
|
- Logically separate feature set
|
||||||
|
- Can be used independently
|
||||||
|
- Different import namespace makes sense
|
||||||
|
- Clear, single responsibility
|
||||||
|
|
||||||
|
### 4. Commands in cmd/
|
||||||
|
|
||||||
|
Executable applications in `cmd/` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/
|
||||||
|
├── onvif-cli/ # Main CLI tool
|
||||||
|
├── onvif-server/ # Virtual camera
|
||||||
|
└── onvif-quick/ # Quick utility
|
||||||
|
```
|
||||||
|
|
||||||
|
**Naming convention:**
|
||||||
|
- Directory name = binary name
|
||||||
|
- Each cmd has its own `main.go`
|
||||||
|
- Can import the library: `import "github.com/0x524a/onvif-go"`
|
||||||
|
|
||||||
|
**Build commands:**
|
||||||
|
```bash
|
||||||
|
go build ./cmd/onvif-cli
|
||||||
|
go build ./cmd/onvif-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Examples for Documentation
|
||||||
|
|
||||||
|
The `examples/` directory demonstrates library usage:
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- Each example is a standalone program
|
||||||
|
- Clear, focused demonstration
|
||||||
|
- Can be built and run directly
|
||||||
|
|
||||||
|
**Purpose:**
|
||||||
|
- Supplement documentation
|
||||||
|
- Show best practices
|
||||||
|
- Provide starting points for users
|
||||||
|
|
||||||
|
### 6. Documentation in docs/
|
||||||
|
|
||||||
|
Comprehensive documentation in `docs/` directory:
|
||||||
|
|
||||||
|
- `ARCHITECTURE.md` - Design and architecture
|
||||||
|
- `PROJECT_STRUCTURE.md` - This file
|
||||||
|
- `SIMPLIFIED_ENDPOINT.md` - Feature documentation
|
||||||
|
- Additional guides as needed
|
||||||
|
|
||||||
|
**Why separate docs/?**
|
||||||
|
- Keeps root clean
|
||||||
|
- Organized by topic
|
||||||
|
- Easy to navigate
|
||||||
|
- Scalable structure
|
||||||
|
|
||||||
|
## Import Patterns
|
||||||
|
|
||||||
|
### Public API (Root Package)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Main client functionality
|
||||||
|
import "github.com/0x524a/onvif-go"
|
||||||
|
|
||||||
|
client, err := onvif.NewClient("192.168.1.100",
|
||||||
|
onvif.WithCredentials("admin", "password"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discovery Subpackage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Device discovery
|
||||||
|
import "github.com/0x524a/onvif-go/discovery"
|
||||||
|
|
||||||
|
devices, err := discovery.Discover(ctx, 5*time.Second)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Subpackage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Virtual ONVIF server
|
||||||
|
import "github.com/0x524a/onvif-go/server"
|
||||||
|
|
||||||
|
srv := server.NewServer(
|
||||||
|
server.WithCredentials("admin", "admin"),
|
||||||
|
server.WithAddress(":8080"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Internal Package (Library Use Only)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Only usable within onvif-go itself
|
||||||
|
import "github.com/0x524a/onvif-go/internal/soap"
|
||||||
|
|
||||||
|
// External projects CANNOT import internal packages
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Organization Best Practices
|
||||||
|
|
||||||
|
### Root Package Files
|
||||||
|
|
||||||
|
Group by service/functionality:
|
||||||
|
- `client.go` - Client creation and core functionality
|
||||||
|
- `device.go` - Device service methods
|
||||||
|
- `media.go` - Media service methods
|
||||||
|
- `ptz.go` - PTZ service methods
|
||||||
|
- `imaging.go` - Imaging service methods
|
||||||
|
- `types.go` - Type definitions
|
||||||
|
- `errors.go` - Error types
|
||||||
|
- `doc.go` - Package documentation
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
|
||||||
|
Co-located with source:
|
||||||
|
- `client_test.go` - Tests for client.go
|
||||||
|
- `device_test.go` - Tests for device.go
|
||||||
|
- Mirrors source file structure
|
||||||
|
|
||||||
|
### Large Packages
|
||||||
|
|
||||||
|
For large packages, consider grouping:
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── server.go # Main server
|
||||||
|
├── device.go # Device handlers
|
||||||
|
├── media.go # Media handlers
|
||||||
|
├── ptz.go # PTZ handlers
|
||||||
|
├── imaging.go # Imaging handlers
|
||||||
|
└── soap/ # SOAP sub-package
|
||||||
|
└── handler.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Other Layouts
|
||||||
|
|
||||||
|
### ❌ Avoid: pkg/ Directory for Libraries
|
||||||
|
|
||||||
|
```
|
||||||
|
# DON'T DO THIS for libraries:
|
||||||
|
my-lib/
|
||||||
|
└── pkg/
|
||||||
|
└── mylib/
|
||||||
|
└── mylib.go
|
||||||
|
|
||||||
|
# Requires: import "github.com/user/my-lib/pkg/mylib"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why not?**
|
||||||
|
- Unnecessary nesting
|
||||||
|
- More complex imports
|
||||||
|
- Not idiomatic for Go libraries
|
||||||
|
- `pkg/` is for applications with multiple packages
|
||||||
|
|
||||||
|
### ✅ Library Layout (What We Use)
|
||||||
|
|
||||||
|
```
|
||||||
|
onvif-go/
|
||||||
|
├── *.go # Public API at root
|
||||||
|
└── internal/ # Private implementation
|
||||||
|
|
||||||
|
# Clean import: import "github.com/user/onvif-go"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Application Layout (Different Use Case)
|
||||||
|
|
||||||
|
For applications (not libraries):
|
||||||
|
```
|
||||||
|
my-app/
|
||||||
|
├── cmd/ # Multiple binaries
|
||||||
|
├── internal/ # Private app code
|
||||||
|
├── pkg/ # Exported libraries from this app
|
||||||
|
└── main.go # Or in cmd/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
### Recent Changes
|
||||||
|
|
||||||
|
**Moved SOAP to internal/:**
|
||||||
|
- `soap/` → `internal/soap/`
|
||||||
|
- Updated imports in:
|
||||||
|
- `device.go`
|
||||||
|
- `media.go`
|
||||||
|
- `ptz.go`
|
||||||
|
- `imaging.go`
|
||||||
|
- `server/soap/handler.go`
|
||||||
|
|
||||||
|
**Reason:**
|
||||||
|
- SOAP client is an implementation detail
|
||||||
|
- Users should interact through high-level API
|
||||||
|
- Prevents tight coupling to SOAP specifics
|
||||||
|
- Allows future protocol changes
|
||||||
|
|
||||||
|
### Import Updates
|
||||||
|
|
||||||
|
**Old:**
|
||||||
|
```go
|
||||||
|
import "github.com/0x524a/onvif-go/soap"
|
||||||
|
```
|
||||||
|
|
||||||
|
**New:**
|
||||||
|
```go
|
||||||
|
import "github.com/0x524a/onvif-go/internal/soap"
|
||||||
|
```
|
||||||
|
|
||||||
|
**External users:** No changes needed (they never imported soap directly)
|
||||||
|
|
||||||
|
## Benefits of This Structure
|
||||||
|
|
||||||
|
### For Library Users
|
||||||
|
|
||||||
|
1. **Simple imports**: `import "github.com/0x524a/onvif-go"`
|
||||||
|
2. **Clear API**: Public vs private clearly separated
|
||||||
|
3. **Stable interface**: Internal changes don't affect users
|
||||||
|
4. **Good documentation**: Examples and docs organized
|
||||||
|
|
||||||
|
### For Contributors
|
||||||
|
|
||||||
|
1. **Clear organization**: Each file has single responsibility
|
||||||
|
2. **Easy navigation**: Logical directory structure
|
||||||
|
3. **Safe refactoring**: Internal package allows changes
|
||||||
|
4. **Standard layout**: Follows Go conventions
|
||||||
|
|
||||||
|
### For Maintenance
|
||||||
|
|
||||||
|
1. **Backward compatibility**: Internal changes don't break users
|
||||||
|
2. **Scalability**: Structure supports growth
|
||||||
|
3. **Testing**: Co-located tests, separate test utilities
|
||||||
|
4. **Documentation**: Organized in docs/
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
As the project grows:
|
||||||
|
|
||||||
|
1. **More subpackages**: Analytics, events, recording services
|
||||||
|
2. **Additional internal packages**: Caching, connection pooling
|
||||||
|
3. **Tool improvements**: Enhanced cmd/ utilities
|
||||||
|
4. **Documentation growth**: More guides in docs/
|
||||||
|
|
||||||
|
The current structure supports these additions naturally.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Standard Go Project Layout](https://github.com/golang-standards/project-layout)
|
||||||
|
- [Go Blog: Package names](https://go.dev/blog/package-names)
|
||||||
|
- [Effective Go](https://go.dev/doc/effective_go)
|
||||||
|
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The onvif-go project structure:
|
||||||
|
- ✅ Follows Go conventions for libraries
|
||||||
|
- ✅ Public API at root level
|
||||||
|
- ✅ Internal package for private code
|
||||||
|
- ✅ Subpackages for additional features
|
||||||
|
- ✅ Clear separation of concerns
|
||||||
|
- ✅ Scalable and maintainable
|
||||||
|
- ✅ User-friendly imports
|
||||||
@@ -191,7 +191,7 @@ Tested/compatible with major brands:
|
|||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/0x524A/go-onvif"
|
import "github.com/0x524a/onvif-go"
|
||||||
|
|
||||||
// Create client
|
// Create client
|
||||||
client, _ := onvif.NewClient(
|
client, _ := onvif.NewClient(
|
||||||
@@ -255,7 +255,7 @@ go-onvif/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install
|
# Install
|
||||||
go get github.com/0x524A/go-onvif
|
go get github.com/0x524a/onvif-go
|
||||||
|
|
||||||
# Run discovery example
|
# Run discovery example
|
||||||
cd examples/discovery
|
cd examples/discovery
|
||||||
@@ -296,4 +296,4 @@ This library is a complete refactoring and modernization inspired by the origina
|
|||||||
|
|
||||||
**Status**: ✅ Production Ready (v0.1.0)
|
**Status**: ✅ Production Ready (v0.1.0)
|
||||||
**Last Updated**: October 2025
|
**Last Updated**: October 2025
|
||||||
**Maintainer**: 0x524A
|
**Maintainer**: 0x524a
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Additional Documentation
|
||||||
|
|
||||||
|
This directory contains supplementary documentation for the onvif-go project.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- **ARCHITECTURE.md** - System architecture and design decisions
|
||||||
|
- **CAMERA_TESTS.md** - Camera testing framework documentation
|
||||||
|
- **IMPLEMENTATION_SUMMARY.md** - Implementation details and notes
|
||||||
|
- **PROJECT_SUMMARY.md** - Project overview and planning
|
||||||
|
- **TEST_QUICKSTART.md** - Testing quickstart guide
|
||||||
|
- **XML_DEBUGGING_SOLUTION.md** - XML debugging tips and solutions
|
||||||
|
|
||||||
|
## Main Documentation
|
||||||
|
|
||||||
|
For primary documentation, see the root directory:
|
||||||
|
|
||||||
|
- [README.md](../README.md) - Main project documentation
|
||||||
|
- [QUICKSTART.md](../QUICKSTART.md) - Getting started guide
|
||||||
|
- [BUILDING.md](../BUILDING.md) - Build and release instructions
|
||||||
|
- [CONTRIBUTING.md](../CONTRIBUTING.md) - Contribution guidelines
|
||||||
|
- [CHANGELOG.md](../CHANGELOG.md) - Version history and changes
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is a comprehensive demonstration of all go-onvif features
|
// This is a comprehensive demonstration of all go-onvif features
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524A/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/discovery"
|
"github.com/0x524a/onvif-go/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0x524a/onvif-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Demonstrates the three different endpoint formats supported by NewClient
|
||||||
|
|
||||||
|
examples := []struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Simple IP",
|
||||||
|
endpoint: "192.168.1.100",
|
||||||
|
desc: "Just the IP address - automatically adds http:// and /onvif/device_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP with Port",
|
||||||
|
endpoint: "192.168.1.100:8080",
|
||||||
|
desc: "IP and port - automatically adds http:// and /onvif/device_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Full URL",
|
||||||
|
endpoint: "http://192.168.1.100/onvif/device_service",
|
||||||
|
desc: "Complete URL - used as-is",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("ONVIF Client - Simplified Endpoint Formats Demo")
|
||||||
|
fmt.Println("================================================")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
for _, ex := range examples {
|
||||||
|
fmt.Printf("%s:\n", ex.name)
|
||||||
|
fmt.Printf(" Input: %s\n", ex.endpoint)
|
||||||
|
fmt.Printf(" Description: %s\n", ex.desc)
|
||||||
|
|
||||||
|
// Create client with simplified endpoint
|
||||||
|
client, err := onvif.NewClient(
|
||||||
|
ex.endpoint,
|
||||||
|
onvif.WithCredentials("admin", "password"),
|
||||||
|
onvif.WithTimeout(5*time.Second),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" Error: %v\n\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" Client created successfully!\n")
|
||||||
|
fmt.Printf(" Endpoint will be: %s\n\n", client.Endpoint())
|
||||||
|
|
||||||
|
// Try to get device information (will fail if camera doesn't exist)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
info, err := client.GetDeviceInformation(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Note: Could not connect to camera (this is expected in demo)\n")
|
||||||
|
fmt.Printf(" Error: %v\n\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Connected to: %s %s\n", info.Manufacturer, info.Model)
|
||||||
|
fmt.Printf(" Firmware: %s\n\n", info.FirmwareVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Key Benefits:")
|
||||||
|
fmt.Println("- Simpler API: Just provide '192.168.1.100' instead of full URL")
|
||||||
|
fmt.Println("- Flexible: Works with IP, IP:port, or full URL")
|
||||||
|
fmt.Println("- Backward Compatible: Existing code continues to work")
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
+112
-139
@@ -6,185 +6,158 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
"github.com/0x524A/go-onvif/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("🧪 Testing ONVIF Server Implementation")
|
fmt.Println("🧪 Testing ONVIF Server with Client Library")
|
||||||
fmt.Println("======================================")
|
fmt.Println("===========================================")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Create and start server in background
|
// Create client
|
||||||
config := server.DefaultConfig()
|
|
||||||
config.Port = 8081 // Use different port to avoid conflicts
|
|
||||||
|
|
||||||
srv, err := server.New(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Start server in background
|
|
||||||
serverReady := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
// Give server a moment to start
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
serverReady <- true
|
|
||||||
|
|
||||||
if err := srv.Start(ctx); err != nil {
|
|
||||||
log.Printf("Server error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for server to be ready
|
|
||||||
<-serverReady
|
|
||||||
fmt.Println("✅ Server started on port 8081")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Create ONVIF client
|
|
||||||
client, err := onvif.NewClient(
|
client, err := onvif.NewClient(
|
||||||
"http://localhost:8081/onvif/device_service",
|
"http://localhost:8080/onvif/device_service",
|
||||||
onvif.WithCredentials("admin", "admin"),
|
onvif.WithCredentials("admin", "admin"),
|
||||||
onvif.WithTimeout(10*time.Second),
|
onvif.WithTimeout(30*time.Second),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create client: %v", err)
|
log.Fatalf("❌ Failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testCtx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Test 1: Get Device Information
|
// Test 1: Get device information
|
||||||
fmt.Println("Test 1: GetDeviceInformation")
|
fmt.Println("📋 Test 1: Getting Device Information...")
|
||||||
info, err := client.GetDeviceInformation(testCtx)
|
info, err := client.GetDeviceInformation(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ GetDeviceInformation failed: %v", err)
|
log.Fatalf("❌ Failed to get device info: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✅ Device: %s %s (Firmware: %s)\n", info.Manufacturer, info.Model, info.FirmwareVersion)
|
fmt.Printf("✅ Device: %s %s\n", info.Manufacturer, info.Model)
|
||||||
|
fmt.Printf(" Firmware: %s\n", info.FirmwareVersion)
|
||||||
fmt.Printf(" Serial: %s\n", info.SerialNumber)
|
fmt.Printf(" Serial: %s\n", info.SerialNumber)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Test 2: Get Capabilities
|
// Test 2: Initialize and discover services
|
||||||
fmt.Println("Test 2: GetCapabilities")
|
fmt.Println("🔍 Test 2: Discovering Services...")
|
||||||
if err := client.Initialize(testCtx); err != nil {
|
if err := client.Initialize(ctx); err != nil {
|
||||||
log.Fatalf("❌ Initialize (GetCapabilities) failed: %v", err)
|
log.Fatalf("❌ Failed to initialize: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("✅ Capabilities retrieved successfully")
|
fmt.Println("✅ Services discovered successfully")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Test 3: Get Profiles
|
// Test 3: Get capabilities
|
||||||
fmt.Println("Test 3: GetProfiles")
|
fmt.Println("🔧 Test 3: Getting Capabilities...")
|
||||||
profiles, err := client.GetProfiles(testCtx)
|
caps, err := client.GetCapabilities(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ GetProfiles failed: %v", err)
|
log.Fatalf("❌ Failed to get capabilities: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✅ Found %d profiles:\n", len(profiles))
|
fmt.Println("✅ Capabilities:")
|
||||||
|
if caps.Media != nil {
|
||||||
|
fmt.Println(" ✓ Media Service")
|
||||||
|
}
|
||||||
|
if caps.PTZ != nil {
|
||||||
|
fmt.Println(" ✓ PTZ Service")
|
||||||
|
}
|
||||||
|
if caps.Imaging != nil {
|
||||||
|
fmt.Println(" ✓ Imaging Service")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Test 4: Get media profiles
|
||||||
|
fmt.Println("🎬 Test 4: Getting Media Profiles...")
|
||||||
|
profiles, err := client.GetProfiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ Failed to get profiles: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Found %d camera profiles:\n", len(profiles))
|
||||||
for i, profile := range profiles {
|
for i, profile := range profiles {
|
||||||
fmt.Printf(" [%d] %s (Token: %s)\n", i+1, profile.Name, profile.Token)
|
fmt.Printf("\n Profile %d: %s\n", i+1, profile.Name)
|
||||||
|
fmt.Printf(" Token: %s\n", profile.Token)
|
||||||
|
|
||||||
if profile.VideoEncoderConfiguration != nil {
|
if profile.VideoEncoderConfiguration != nil {
|
||||||
fmt.Printf(" Video: %dx%d @ %s\n",
|
fmt.Printf(" Video: %dx%d @ %s\n",
|
||||||
profile.VideoEncoderConfiguration.Resolution.Width,
|
profile.VideoEncoderConfiguration.Resolution.Width,
|
||||||
profile.VideoEncoderConfiguration.Resolution.Height,
|
profile.VideoEncoderConfiguration.Resolution.Height,
|
||||||
profile.VideoEncoderConfiguration.Encoding)
|
profile.VideoEncoderConfiguration.Encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get stream URI
|
||||||
|
streamURI, err := client.GetStreamURI(ctx, profile.Token)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ⚠️ Failed to get stream URI: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" RTSP: %s\n", streamURI.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get snapshot URI if available
|
||||||
|
snapshotURI, err := client.GetSnapshotURI(ctx, profile.Token)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf(" Snapshot: %s\n", snapshotURI.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test PTZ if available
|
||||||
|
if profile.PTZConfiguration != nil {
|
||||||
|
fmt.Println(" PTZ: ✓ Enabled")
|
||||||
|
|
||||||
|
// Get PTZ status
|
||||||
|
status, err := client.GetStatus(ctx, profile.Token)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf(" Position: Pan=%.1f°, Tilt=%.1f°, Zoom=%.2f\n",
|
||||||
|
status.Position.PanTilt.X,
|
||||||
|
status.Position.PanTilt.Y,
|
||||||
|
status.Position.Zoom.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get presets
|
||||||
|
presets, err := client.GetPresets(ctx, profile.Token)
|
||||||
|
if err == nil && len(presets) > 0 {
|
||||||
|
fmt.Printf(" Presets: %d available\n", len(presets))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Test 4: Get Stream URI
|
// Test 5: PTZ control (if available)
|
||||||
if len(profiles) > 0 {
|
|
||||||
fmt.Println("Test 4: GetStreamURI")
|
|
||||||
streamURI, err := client.GetStreamURI(testCtx, profiles[0].Token)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ GetStreamURI failed: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Stream URI: %s\n", streamURI.URI)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Get Snapshot URI
|
|
||||||
if len(profiles) > 0 {
|
|
||||||
fmt.Println("Test 5: GetSnapshotURI")
|
|
||||||
snapshotURI, err := client.GetSnapshotURI(testCtx, profiles[0].Token)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ GetSnapshotURI failed: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Snapshot URI: %s\n", snapshotURI.URI)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 6: PTZ Status (if PTZ is available)
|
|
||||||
if len(profiles) > 0 && profiles[0].PTZConfiguration != nil {
|
if len(profiles) > 0 && profiles[0].PTZConfiguration != nil {
|
||||||
fmt.Println("Test 6: PTZ GetStatus")
|
fmt.Println("🎮 Test 5: Testing PTZ Control...")
|
||||||
status, err := client.GetStatus(testCtx, profiles[0].Token)
|
profileToken := profiles[0].Token
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ GetStatus failed: %v", err)
|
// Absolute move to home position
|
||||||
}
|
fmt.Println(" Moving to home position...")
|
||||||
fmt.Printf("✅ PTZ Position: Pan=%.2f, Tilt=%.2f, Zoom=%.2f\n",
|
|
||||||
status.Position.PanTilt.X,
|
|
||||||
status.Position.PanTilt.Y,
|
|
||||||
status.Position.Zoom.X)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 7: PTZ Absolute Move
|
|
||||||
fmt.Println("Test 7: PTZ AbsoluteMove")
|
|
||||||
position := &onvif.PTZVector{
|
position := &onvif.PTZVector{
|
||||||
PanTilt: &onvif.Vector2D{X: 10.0, Y: -5.0},
|
PanTilt: &onvif.Vector2D{X: 0.0, Y: 0.0},
|
||||||
Zoom: &onvif.Vector1D{X: 0.5},
|
Zoom: &onvif.Vector1D{X: 0.0},
|
||||||
}
|
}
|
||||||
if err := client.AbsoluteMove(testCtx, profiles[0].Token, position, nil); err != nil {
|
if err := client.AbsoluteMove(ctx, profileToken, position, nil); err != nil {
|
||||||
log.Fatalf("❌ AbsoluteMove failed: %v", err)
|
fmt.Printf(" ⚠️ Failed to move: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ✅ Moved to home position")
|
||||||
}
|
}
|
||||||
fmt.Println("✅ PTZ moved to absolute position")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Wait a bit for movement to complete
|
// Wait a moment
|
||||||
time.Sleep(600 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
// Verify new position
|
// Get status after move
|
||||||
fmt.Println("Test 8: Verify PTZ Position")
|
status, err := client.GetStatus(ctx, profileToken)
|
||||||
status, err = client.GetStatus(testCtx, profiles[0].Token)
|
if err == nil {
|
||||||
if err != nil {
|
fmt.Printf(" New position: Pan=%.1f°, Tilt=%.1f°, Zoom=%.2f\n",
|
||||||
log.Fatalf("❌ GetStatus failed: %v", err)
|
status.Position.PanTilt.X,
|
||||||
}
|
status.Position.PanTilt.Y,
|
||||||
fmt.Printf("✅ New PTZ Position: Pan=%.2f, Tilt=%.2f, Zoom=%.2f\n",
|
status.Position.Zoom.X)
|
||||||
status.Position.PanTilt.X,
|
|
||||||
status.Position.PanTilt.Y,
|
|
||||||
status.Position.Zoom.X)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 9: PTZ Presets
|
|
||||||
fmt.Println("Test 9: Get PTZ Presets")
|
|
||||||
presets, err := client.GetPresets(testCtx, profiles[0].Token)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ GetPresets failed: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Found %d presets:\n", len(presets))
|
|
||||||
for i, preset := range presets {
|
|
||||||
fmt.Printf(" [%d] %s (Token: %s)\n", i+1, preset.Name, preset.Token)
|
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 10: Get System Date and Time
|
// Summary
|
||||||
fmt.Println("Test 10: GetSystemDateAndTime")
|
fmt.Println("╔════════════════════════════════════════════════════════════╗")
|
||||||
_, err = client.GetSystemDateAndTime(testCtx)
|
fmt.Println("║ ║")
|
||||||
if err != nil {
|
fmt.Println("║ ✅ All Tests Passed! ✅ ║")
|
||||||
log.Fatalf("❌ GetSystemDateAndTime failed: %v", err)
|
fmt.Println("║ ║")
|
||||||
}
|
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||||
fmt.Println("✅ System date and time retrieved successfully")
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
fmt.Println("🎉 ONVIF Server is working correctly!")
|
||||||
// All tests passed!
|
fmt.Println(" • Device Service: ✓")
|
||||||
fmt.Println("╔══════════════════════════════════════════════════════════╗")
|
fmt.Println(" • Media Service: ✓")
|
||||||
fmt.Println("║ ║")
|
fmt.Println(" • PTZ Service: ✓")
|
||||||
fmt.Println("║ ✅ All Tests Passed! ONVIF Server is working! ✅ ║")
|
fmt.Printf(" • Multi-lens Camera: ✓ (%d profiles)\n", len(profiles))
|
||||||
fmt.Println("║ ║")
|
|
||||||
fmt.Println("╚══════════════════════════════════════════════════════════╝")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Stop the server
|
|
||||||
cancel()
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module github.com/0x524A/go-onvif
|
module github.com/0x524a/onvif-go
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/soap"
|
"github.com/0x524a/onvif-go/internal/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Imaging service namespace
|
// Imaging service namespace
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/soap"
|
"github.com/0x524a/onvif-go/internal/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Media service namespace
|
// Media service namespace
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/soap"
|
"github.com/0x524a/onvif-go/internal/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PTZ service namespace
|
// PTZ service namespace
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Test script for running ONVIF camera integration tests
|
|
||||||
# Usage: ./run-camera-tests.sh [test-name]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Color output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== ONVIF Camera Integration Tests ===${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Check if environment variables are set
|
|
||||||
if [ -z "$ONVIF_TEST_ENDPOINT" ] || [ -z "$ONVIF_TEST_USERNAME" ] || [ -z "$ONVIF_TEST_PASSWORD" ]; then
|
|
||||||
echo -e "${YELLOW}Warning: Camera credentials not set${NC}"
|
|
||||||
echo "Set the following environment variables:"
|
|
||||||
echo " export ONVIF_TEST_ENDPOINT=\"http://192.168.1.201/onvif/device_service\""
|
|
||||||
echo " export ONVIF_TEST_USERNAME=\"service\""
|
|
||||||
echo " export ONVIF_TEST_PASSWORD=\"Service.1234\""
|
|
||||||
echo
|
|
||||||
echo -e "${YELLOW}Tests will be skipped.${NC}"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine which tests to run
|
|
||||||
TEST_PATTERN="${1:-TestBoschFLEXIDOMEIndoor5100iIR}"
|
|
||||||
|
|
||||||
echo -e "${GREEN}Running tests matching: ${TEST_PATTERN}${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run tests with verbose output
|
|
||||||
go test -v -run "$TEST_PATTERN" -timeout 60s
|
|
||||||
|
|
||||||
# Check exit code
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo
|
|
||||||
echo -e "${GREEN}✓ All tests passed!${NC}"
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo -e "${RED}✗ Some tests failed${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -213,7 +213,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
+5
-5
@@ -41,7 +41,7 @@ A complete ONVIF-compliant server implementation that simulates multi-lens IP ca
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository (if not already done)
|
# Clone the repository (if not already done)
|
||||||
git clone https://github.com/0x524A/go-onvif
|
git clone https://github.com/0x524a/onvif-go
|
||||||
cd go-onvif
|
cd go-onvif
|
||||||
|
|
||||||
# Build the server CLI
|
# Build the server CLI
|
||||||
@@ -128,7 +128,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -164,7 +164,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server"
|
"github.com/0x524a/onvif-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -251,7 +251,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -430,7 +430,7 @@ This project is licensed under the MIT License - see the [LICENSE](../../LICENSE
|
|||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- Built on top of the [go-onvif](https://github.com/0x524A/go-onvif) client library
|
- Built on top of the [go-onvif](https://github.com/0x524a/onvif-go) client library
|
||||||
- ONVIF specifications from [ONVIF.org](https://www.onvif.org)
|
- ONVIF specifications from [ONVIF.org](https://www.onvif.org)
|
||||||
- Inspired by the need for flexible camera simulation in development workflows
|
- Inspired by the need for flexible camera simulation in development workflows
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server/soap"
|
"github.com/0x524a/onvif-go/server/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Device service SOAP message types
|
// Device service SOAP message types
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif/server/soap"
|
"github.com/0x524a/onvif-go/server/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates a new ONVIF server with the given configuration
|
// New creates a new ONVIF server with the given configuration
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
originsoap "github.com/0x524A/go-onvif/soap"
|
originsoap "github.com/0x524a/onvif-go/internal/soap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler handles incoming SOAP requests
|
// Handler handles incoming SOAP requests
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents the ONVIF server configuration
|
// Config represents the ONVIF server configuration
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("🧪 Testing ONVIF Server with Client Library")
|
|
||||||
fmt.Println("===========================================")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Create client
|
|
||||||
client, err := onvif.NewClient(
|
|
||||||
"http://localhost:8080/onvif/device_service",
|
|
||||||
onvif.WithCredentials("admin", "admin"),
|
|
||||||
onvif.WithTimeout(30*time.Second),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ Failed to create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Test 1: Get device information
|
|
||||||
fmt.Println("📋 Test 1: Getting Device Information...")
|
|
||||||
info, err := client.GetDeviceInformation(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ Failed to get device info: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Device: %s %s\n", info.Manufacturer, info.Model)
|
|
||||||
fmt.Printf(" Firmware: %s\n", info.FirmwareVersion)
|
|
||||||
fmt.Printf(" Serial: %s\n", info.SerialNumber)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 2: Initialize and discover services
|
|
||||||
fmt.Println("🔍 Test 2: Discovering Services...")
|
|
||||||
if err := client.Initialize(ctx); err != nil {
|
|
||||||
log.Fatalf("❌ Failed to initialize: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✅ Services discovered successfully")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 3: Get capabilities
|
|
||||||
fmt.Println("🔧 Test 3: Getting Capabilities...")
|
|
||||||
caps, err := client.GetCapabilities(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ Failed to get capabilities: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✅ Capabilities:")
|
|
||||||
if caps.Media != nil {
|
|
||||||
fmt.Println(" ✓ Media Service")
|
|
||||||
}
|
|
||||||
if caps.PTZ != nil {
|
|
||||||
fmt.Println(" ✓ PTZ Service")
|
|
||||||
}
|
|
||||||
if caps.Imaging != nil {
|
|
||||||
fmt.Println(" ✓ Imaging Service")
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 4: Get media profiles
|
|
||||||
fmt.Println("🎬 Test 4: Getting Media Profiles...")
|
|
||||||
profiles, err := client.GetProfiles(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("❌ Failed to get profiles: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Found %d camera profiles:\n", len(profiles))
|
|
||||||
for i, profile := range profiles {
|
|
||||||
fmt.Printf("\n Profile %d: %s\n", i+1, profile.Name)
|
|
||||||
fmt.Printf(" Token: %s\n", profile.Token)
|
|
||||||
|
|
||||||
if profile.VideoEncoderConfiguration != nil {
|
|
||||||
fmt.Printf(" Video: %dx%d @ %s\n",
|
|
||||||
profile.VideoEncoderConfiguration.Resolution.Width,
|
|
||||||
profile.VideoEncoderConfiguration.Resolution.Height,
|
|
||||||
profile.VideoEncoderConfiguration.Encoding)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get stream URI
|
|
||||||
streamURI, err := client.GetStreamURI(ctx, profile.Token)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" ⚠️ Failed to get stream URI: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" RTSP: %s\n", streamURI.URI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get snapshot URI if available
|
|
||||||
snapshotURI, err := client.GetSnapshotURI(ctx, profile.Token)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf(" Snapshot: %s\n", snapshotURI.URI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test PTZ if available
|
|
||||||
if profile.PTZConfiguration != nil {
|
|
||||||
fmt.Println(" PTZ: ✓ Enabled")
|
|
||||||
|
|
||||||
// Get PTZ status
|
|
||||||
status, err := client.GetStatus(ctx, profile.Token)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf(" Position: Pan=%.1f°, Tilt=%.1f°, Zoom=%.2f\n",
|
|
||||||
status.Position.PanTilt.X,
|
|
||||||
status.Position.PanTilt.Y,
|
|
||||||
status.Position.Zoom.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get presets
|
|
||||||
presets, err := client.GetPresets(ctx, profile.Token)
|
|
||||||
if err == nil && len(presets) > 0 {
|
|
||||||
fmt.Printf(" Presets: %d available\n", len(presets))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Test 5: PTZ control (if available)
|
|
||||||
if len(profiles) > 0 && profiles[0].PTZConfiguration != nil {
|
|
||||||
fmt.Println("🎮 Test 5: Testing PTZ Control...")
|
|
||||||
profileToken := profiles[0].Token
|
|
||||||
|
|
||||||
// Absolute move to home position
|
|
||||||
fmt.Println(" Moving to home position...")
|
|
||||||
position := &onvif.PTZVector{
|
|
||||||
PanTilt: &onvif.Vector2D{X: 0.0, Y: 0.0},
|
|
||||||
Zoom: &onvif.Vector1D{X: 0.0},
|
|
||||||
}
|
|
||||||
if err := client.AbsoluteMove(ctx, profileToken, position, nil); err != nil {
|
|
||||||
fmt.Printf(" ⚠️ Failed to move: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ✅ Moved to home position")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a moment
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
// Get status after move
|
|
||||||
status, err := client.GetStatus(ctx, profileToken)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf(" New position: Pan=%.1f°, Tilt=%.1f°, Zoom=%.2f\n",
|
|
||||||
status.Position.PanTilt.X,
|
|
||||||
status.Position.PanTilt.Y,
|
|
||||||
status.Position.Zoom.X)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
fmt.Println("╔════════════════════════════════════════════════════════════╗")
|
|
||||||
fmt.Println("║ ║")
|
|
||||||
fmt.Println("║ ✅ All Tests Passed! ✅ ║")
|
|
||||||
fmt.Println("║ ║")
|
|
||||||
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("🎉 ONVIF Server is working correctly!")
|
|
||||||
fmt.Println(" • Device Service: ✓")
|
|
||||||
fmt.Println(" • Media Service: ✓")
|
|
||||||
fmt.Println(" • PTZ Service: ✓")
|
|
||||||
fmt.Printf(" • Multi-lens Camera: ✓ (%d profiles)\n", len(profiles))
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
onviftesting "github.com/0x524A/go-onvif/testing"
|
onviftesting "github.com/0x524a/onvif-go/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestBosch_FLEXIDOME_indoor_5100i_IR_8710066 tests ONVIF client against Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066 captured responses
|
// TestBosch_FLEXIDOME_indoor_5100i_IR_8710066 tests ONVIF client against Bosch_FLEXIDOME_indoor_5100i_IR_8.71.0066 captured responses
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0x524A/go-onvif"
|
"github.com/0x524a/onvif-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestEnhancedDeviceFeatures tests new Device service methods with real camera data
|
// TestEnhancedDeviceFeatures tests new Device service methods with real camera data
|
||||||
|
|||||||
Reference in New Issue
Block a user