修改视频RTSP连接
parent
0a118e73bf
commit
e9b491dd08
@ -0,0 +1,21 @@
|
||||
CAMERA_HOST=192.168.1.68
|
||||
CAMERA_PORT=554
|
||||
CAMERA_USER=admin
|
||||
CAMERA_PASSWORD=change-me
|
||||
CAMERA_RTSP_PATH=/Stream/Live/101
|
||||
|
||||
WS_HOST=0.0.0.0
|
||||
WS_PORT=8080
|
||||
WS_PATH=/video
|
||||
RTSP_TRANSPORT=tcp
|
||||
VIDEO_TRANSCODE=0
|
||||
FFMPEG_PATH=ffmpeg
|
||||
|
||||
CONTROL_HOST=0.0.0.0
|
||||
CONTROL_PORT=8090
|
||||
CONTROL_TRANSPORT=tcp
|
||||
CONTROL_TCP_HOST=192.168.1.123
|
||||
CONTROL_TCP_PORT=502
|
||||
CONTROL_ADDRESS=1
|
||||
CONTROL_SPEED=32
|
||||
VITE_CONTROL_API_URL=http://localhost:8090/control
|
||||
@ -0,0 +1,388 @@
|
||||
import dgram from 'node:dgram'
|
||||
import {existsSync, readFileSync} from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import net from 'node:net'
|
||||
import {resolve} from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
loadDotEnv()
|
||||
|
||||
const DEFAULT_ADDRESS = 0x01
|
||||
const DEFAULT_SPEED = 0x20
|
||||
|
||||
const CONTROL_HOST = process.env.CONTROL_HOST || '0.0.0.0'
|
||||
const CONTROL_PORT = Number(process.env.CONTROL_PORT || 8090)
|
||||
const CONTROL_TRANSPORT = process.env.CONTROL_TRANSPORT || 'log'
|
||||
const CONTROL_TCP_HOST = process.env.CONTROL_TCP_HOST || ''
|
||||
const CONTROL_TCP_PORT = Number(process.env.CONTROL_TCP_PORT || 0)
|
||||
const CONTROL_UDP_HOST = process.env.CONTROL_UDP_HOST || ''
|
||||
const CONTROL_UDP_PORT = Number(process.env.CONTROL_UDP_PORT || 0)
|
||||
const CONTROL_ADDRESS = parseByte(process.env.CONTROL_ADDRESS, DEFAULT_ADDRESS)
|
||||
const CONTROL_SPEED = parseByte(process.env.CONTROL_SPEED, DEFAULT_SPEED)
|
||||
|
||||
function loadDotEnv() {
|
||||
const envPath = resolve(process.cwd(), '.env')
|
||||
|
||||
if (!existsSync(envPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = readFileSync(envPath, 'utf8').split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const separatorIndex = trimmed.indexOf('=')
|
||||
if (separatorIndex === -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
let value = trimmed.slice(separatorIndex + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseByte(value, fallback) {
|
||||
if (!value) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const normalized = String(value).trim().toLowerCase()
|
||||
const parsed = normalized.startsWith('0x')
|
||||
? Number.parseInt(normalized.slice(2), 16)
|
||||
: Number.parseInt(normalized, 10)
|
||||
|
||||
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 0xff) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
function clampPelcoSpeed(value) {
|
||||
return Math.max(0x00, Math.min(0x3f, value))
|
||||
}
|
||||
|
||||
function createPelcoDCommand(command1, command2, data1 = 0x00, data2 = 0x00) {
|
||||
const address = CONTROL_ADDRESS
|
||||
const checksum = (address + command1 + command2 + data1 + data2) & 0xff
|
||||
return Buffer.from([
|
||||
0xff,
|
||||
address,
|
||||
command1,
|
||||
command2,
|
||||
data1,
|
||||
data2,
|
||||
checksum,
|
||||
])
|
||||
}
|
||||
|
||||
function createPresetCommand(operation, presetNo) {
|
||||
const preset = parseByte(presetNo, 0x00)
|
||||
|
||||
if (preset < 0x01 || preset > 0xff) {
|
||||
throw new Error(`Invalid preset number: ${presetNo}`)
|
||||
}
|
||||
|
||||
const command2ByOperation = {
|
||||
set: 0x03,
|
||||
clear: 0x05,
|
||||
call: 0x07,
|
||||
}
|
||||
const command2 = command2ByOperation[operation]
|
||||
|
||||
if (command2 === undefined) {
|
||||
throw new Error(`Invalid preset operation: ${operation}`)
|
||||
}
|
||||
|
||||
return createPelcoDCommand(0x00, command2, 0x00, preset)
|
||||
}
|
||||
|
||||
export const CONTROL_COMMANDS = Object.freeze({
|
||||
PTZ_UP: 'PTZ_UP',
|
||||
PTZ_DOWN: 'PTZ_DOWN',
|
||||
PTZ_LEFT: 'PTZ_LEFT',
|
||||
PTZ_RIGHT: 'PTZ_RIGHT',
|
||||
PTZ_STOP: 'PTZ_STOP',
|
||||
ZOOM_TELE: 'ZOOM_TELE',
|
||||
ZOOM_WIDE: 'ZOOM_WIDE',
|
||||
FOCUS_FAR: 'FOCUS_FAR',
|
||||
FOCUS_NEAR: 'FOCUS_NEAR',
|
||||
IRIS_OPEN: 'IRIS_OPEN',
|
||||
IRIS_CLOSE: 'IRIS_CLOSE',
|
||||
AUX_2_ON: 'AUX_2_ON',
|
||||
AUX_2_OFF: 'AUX_2_OFF',
|
||||
PRESET_1_CALL: 'PRESET_1_CALL',
|
||||
LASER_ON: 'LASER_ON',
|
||||
LASER_AUTO: 'LASER_AUTO',
|
||||
LASER_OFF: 'LASER_OFF',
|
||||
AUTO_FOCUS_ON: 'AUTO_FOCUS_ON',
|
||||
AUTO_FOCUS_OFF: 'AUTO_FOCUS_OFF',
|
||||
DEFOG_ON: 'DEFOG_ON',
|
||||
DEFOG_OFF: 'DEFOG_OFF',
|
||||
DEFROST_ON: 'DEFROST_ON',
|
||||
DEFROST_OFF: 'DEFROST_OFF',
|
||||
REBOOT: 'REBOOT',
|
||||
})
|
||||
|
||||
export function buildControlCommand(action) {
|
||||
const speed = clampPelcoSpeed(CONTROL_SPEED)
|
||||
|
||||
switch (action) {
|
||||
case CONTROL_COMMANDS.PTZ_UP:
|
||||
return createPelcoDCommand(0x00, 0x08, 0x00, speed)
|
||||
case CONTROL_COMMANDS.PTZ_DOWN:
|
||||
return createPelcoDCommand(0x00, 0x10, 0x00, speed)
|
||||
case CONTROL_COMMANDS.PTZ_LEFT:
|
||||
return createPelcoDCommand(0x00, 0x04, speed, 0x00)
|
||||
case CONTROL_COMMANDS.PTZ_RIGHT:
|
||||
return createPelcoDCommand(0x00, 0x02, speed, 0x00)
|
||||
case CONTROL_COMMANDS.PTZ_STOP:
|
||||
return createPelcoDCommand(0x00, 0x00, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.ZOOM_TELE:
|
||||
return createPelcoDCommand(0x00, 0x20, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.ZOOM_WIDE:
|
||||
return createPelcoDCommand(0x00, 0x40, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.FOCUS_FAR:
|
||||
return createPelcoDCommand(0x00, 0x80, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.FOCUS_NEAR:
|
||||
return createPelcoDCommand(0x01, 0x00, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.IRIS_OPEN:
|
||||
return createPelcoDCommand(0x02, 0x00, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.IRIS_CLOSE:
|
||||
return createPelcoDCommand(0x04, 0x00, 0x00, 0x00)
|
||||
case CONTROL_COMMANDS.AUX_2_ON:
|
||||
return createPelcoDCommand(0x00, 0x09, 0x00, 0x02)
|
||||
case CONTROL_COMMANDS.AUX_2_OFF:
|
||||
return createPelcoDCommand(0x00, 0x0b, 0x00, 0x02)
|
||||
case CONTROL_COMMANDS.PRESET_1_CALL:
|
||||
return createPresetCommand('call', 1)
|
||||
case CONTROL_COMMANDS.LASER_ON:
|
||||
return createPresetCommand('call', 224)
|
||||
case CONTROL_COMMANDS.LASER_AUTO:
|
||||
return createPresetCommand('call', 225)
|
||||
case CONTROL_COMMANDS.LASER_OFF:
|
||||
return createPresetCommand('call', 226)
|
||||
case CONTROL_COMMANDS.AUTO_FOCUS_ON:
|
||||
return createPresetCommand('call', 135)
|
||||
case CONTROL_COMMANDS.AUTO_FOCUS_OFF:
|
||||
return createPresetCommand('call', 136)
|
||||
case CONTROL_COMMANDS.DEFOG_ON:
|
||||
return createPresetCommand('call', 252)
|
||||
case CONTROL_COMMANDS.DEFOG_OFF:
|
||||
return createPresetCommand('call', 253)
|
||||
case CONTROL_COMMANDS.DEFROST_ON:
|
||||
return createPresetCommand('call', 250)
|
||||
case CONTROL_COMMANDS.DEFROST_OFF:
|
||||
return createPresetCommand('call', 251)
|
||||
case CONTROL_COMMANDS.REBOOT:
|
||||
return createPresetCommand('call', 220)
|
||||
default:
|
||||
throw new Error(`Unsupported control action: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
function writeTcp(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!CONTROL_TCP_HOST || !CONTROL_TCP_PORT) {
|
||||
reject(new Error('Missing CONTROL_TCP_HOST or CONTROL_TCP_PORT'))
|
||||
return
|
||||
}
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: CONTROL_TCP_HOST,
|
||||
port: CONTROL_TCP_PORT,
|
||||
})
|
||||
|
||||
socket.setTimeout(3000)
|
||||
socket.on('connect', () => {
|
||||
socket.end(command)
|
||||
})
|
||||
socket.on('close', resolve)
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy(new Error('Control TCP connection timed out'))
|
||||
})
|
||||
socket.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
function writeUdp(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!CONTROL_UDP_HOST || !CONTROL_UDP_PORT) {
|
||||
reject(new Error('Missing CONTROL_UDP_HOST or CONTROL_UDP_PORT'))
|
||||
return
|
||||
}
|
||||
|
||||
const socket = dgram.createSocket('udp4')
|
||||
socket.send(command, CONTROL_UDP_PORT, CONTROL_UDP_HOST, (error) => {
|
||||
socket.close()
|
||||
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendControlAction(action) {
|
||||
const command = buildControlCommand(action)
|
||||
const hex = command.toString('hex').match(/../g).join(' ').toUpperCase()
|
||||
|
||||
if (CONTROL_TRANSPORT === 'tcp') {
|
||||
await writeTcp(command)
|
||||
} else if (CONTROL_TRANSPORT === 'udp') {
|
||||
await writeUdp(command)
|
||||
} else {
|
||||
console.log(`[control] dry-run ${action}: ${hex}`)
|
||||
}
|
||||
|
||||
return {
|
||||
action,
|
||||
hex,
|
||||
transport: CONTROL_TRANSPORT,
|
||||
}
|
||||
}
|
||||
|
||||
export function getControlStatus() {
|
||||
return {
|
||||
host: CONTROL_HOST,
|
||||
port: CONTROL_PORT,
|
||||
transport: CONTROL_TRANSPORT,
|
||||
address: CONTROL_ADDRESS,
|
||||
speed: clampPelcoSpeed(CONTROL_SPEED),
|
||||
tcpConfigured: Boolean(CONTROL_TCP_HOST && CONTROL_TCP_PORT),
|
||||
udpConfigured: Boolean(CONTROL_UDP_HOST && CONTROL_UDP_PORT),
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(response, statusCode, payload) {
|
||||
response.writeHead(statusCode, {
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-methods': 'GET,POST,OPTIONS',
|
||||
'access-control-allow-headers': 'content-type',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
})
|
||||
response.end(JSON.stringify(payload))
|
||||
}
|
||||
|
||||
function readRequestJson(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = ''
|
||||
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk) => {
|
||||
body += chunk
|
||||
|
||||
if (body.length > 1024 * 1024) {
|
||||
request.destroy(new Error('Request body is too large'))
|
||||
}
|
||||
})
|
||||
request.on('end', () => {
|
||||
if (!body.trim()) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(body))
|
||||
} catch {
|
||||
reject(new Error('Invalid JSON body'))
|
||||
}
|
||||
})
|
||||
request.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleControlRequest(request, response) {
|
||||
try {
|
||||
const payload = await readRequestJson(request)
|
||||
const action = payload.action
|
||||
|
||||
if (!action || typeof action !== 'string') {
|
||||
writeJson(response, 400, {
|
||||
ok: false,
|
||||
message: 'Missing action',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await sendControlAction(action)
|
||||
writeJson(response, 200, {
|
||||
ok: true,
|
||||
...result,
|
||||
})
|
||||
} catch (error) {
|
||||
writeJson(response, 400, {
|
||||
ok: false,
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function startControlServer() {
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.method === 'OPTIONS') {
|
||||
writeJson(response, 204, {})
|
||||
return
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && request.url === '/health') {
|
||||
writeJson(response, 200, {
|
||||
ok: true,
|
||||
control: getControlStatus(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && request.url === '/commands') {
|
||||
writeJson(response, 200, {
|
||||
ok: true,
|
||||
commands: Object.values(CONTROL_COMMANDS),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (request.method === 'POST' && request.url === '/control') {
|
||||
handleControlRequest(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
writeJson(response, 404, {
|
||||
ok: false,
|
||||
message: 'Not Found',
|
||||
})
|
||||
})
|
||||
|
||||
server.on('error', (error) => {
|
||||
console.error(`[control] failed to listen on ${CONTROL_HOST}:${CONTROL_PORT}: ${error.message}`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
|
||||
server.listen(CONTROL_PORT, CONTROL_HOST, () => {
|
||||
console.log(`[control] http://${CONTROL_HOST}:${CONTROL_PORT}`)
|
||||
console.log(`[control] transport=${CONTROL_TRANSPORT}`)
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
const currentFile = fileURLToPath(import.meta.url)
|
||||
|
||||
if (process.argv[1] === currentFile) {
|
||||
startControlServer()
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
const DEFAULT_SPEED = 50
|
||||
|
||||
export function stopPt() {
|
||||
return request({
|
||||
url: '/api/Pt/Stop',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function moveUp(speed = DEFAULT_SPEED) {
|
||||
return request({
|
||||
url: '/api/Pt/MoveUp',
|
||||
method: 'post',
|
||||
params: { speed },
|
||||
})
|
||||
}
|
||||
|
||||
export function moveDown(speed = DEFAULT_SPEED) {
|
||||
return request({
|
||||
url: '/api/Pt/MoveDown',
|
||||
method: 'post',
|
||||
params: { speed },
|
||||
})
|
||||
}
|
||||
|
||||
export function moveLeft(speed = DEFAULT_SPEED) {
|
||||
return request({
|
||||
url: '/api/Pt/MoveLeft',
|
||||
method: 'post',
|
||||
params: { speed },
|
||||
})
|
||||
}
|
||||
|
||||
export function moveRight(speed = DEFAULT_SPEED) {
|
||||
return request({
|
||||
url: '/api/Pt/MoveRight',
|
||||
method: 'post',
|
||||
params: { speed },
|
||||
})
|
||||
}
|
||||
|
||||
export function zoomTele() {
|
||||
return request({
|
||||
url: '/api/Pt/ZoomTele',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function zoomWide() {
|
||||
return request({
|
||||
url: '/api/Pt/ZoomWide',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function callPreset1() {
|
||||
return request({
|
||||
url: '/api/Pt/CallPreset',
|
||||
method: 'post',
|
||||
params: { presetNo: 1 },
|
||||
})
|
||||
}
|
||||
|
||||
export function laserOn() {
|
||||
return request({
|
||||
url: '/api/Pt/LaserOn',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function laserAuto() {
|
||||
return request({
|
||||
url: '/api/Pt/LaserAuto',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function laserOff() {
|
||||
return request({
|
||||
url: '/api/Pt/LaserOff',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
export function autoFocusOn() {
|
||||
return request({
|
||||
url: '/api/Pt/CallPreset',
|
||||
method: 'post',
|
||||
params: { presetNo: 135 },
|
||||
})
|
||||
}
|
||||
|
||||
export function defogOn() {
|
||||
return request({
|
||||
url: '/api/Pt/CallPreset',
|
||||
method: 'post',
|
||||
params: { presetNo: 252 },
|
||||
})
|
||||
}
|
||||
|
||||
export function defrostOn() {
|
||||
return request({
|
||||
url: '/api/Pt/CallPreset',
|
||||
method: 'post',
|
||||
params: { presetNo: 250 },
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
(config) => config,
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
export function request(config) {
|
||||
return service(config)
|
||||
}
|
||||
|
||||
export default request
|
||||
Loading…
Reference in New Issue