mirror of
https://github.com/gowvp/gb28181.git
synced 2026-04-22 15:07:10 +08:00
AI
This commit is contained in:
@@ -31,6 +31,7 @@ init:
|
||||
go install github.com/divan/expvarmon@latest
|
||||
go install github.com/rakyll/hey@latest
|
||||
go install mvdan.cc/gofumpt@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
## wire: 生成依赖注入代码
|
||||
wire:
|
||||
@@ -50,6 +51,22 @@ expva/db:
|
||||
# 发起 100 次请求,每次并发 50
|
||||
# hey -n 100 -c 50 http://localhost:9999/healthcheck
|
||||
|
||||
# --go-grpc_out=
|
||||
protobuf:
|
||||
@protoc \
|
||||
--go_out=. \
|
||||
--go-grpc_out=. \
|
||||
./protos/*.proto
|
||||
@python -m grpc_tools.protoc \
|
||||
-I./protos \
|
||||
--python_out=./analysis \
|
||||
--grpc_python_out=./analysis \
|
||||
--pyi_out=./analysis \
|
||||
./protos/*.proto
|
||||
|
||||
python/init:
|
||||
@pip install -r python/requirements.txt
|
||||
|
||||
|
||||
# ==================================================================================== #
|
||||
# QUALITY CONTROL
|
||||
|
||||
@@ -113,6 +113,25 @@ proxy_set_header Upgrade $http_upgrade;
|
||||
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
> How to use other databases?
|
||||
|
||||
In the `configs/config.toml` configuration file, modify `database.dsn`
|
||||
|
||||
[Recommended] SQLite should be a local disk path, default is `configs/data.db`
|
||||
|
||||
[Recommended] PostgreSQL format: `postgres://postgres:123456@127.0.0.1:5432/gb28181?sslmode=disable`
|
||||
|
||||
MySQL format: `mysql://root:123456@127.0.0.1:5432/gb28181?sslmode=disable`
|
||||
|
||||
PostgreSQL and MySQL format pattern:
|
||||
`<db_type>://<username>:<password>@<ip>:<port>/<db_name>?sslmode=disable`
|
||||
|
||||
> How to disable AI?
|
||||
|
||||
AI detection is enabled by default, detecting 5 frames per second.
|
||||
|
||||
You can disable AI detection by setting `disabledAI = true` in `configs/config.toml`
|
||||
|
||||
## Documentation
|
||||
|
||||
GoWVP [Online API Documentation](https://apifox.com/apidoc/shared-7b67c918-5f72-4f64-b71d-0593d7427b93)
|
||||
|
||||
@@ -116,6 +116,26 @@ proxy_set_header Upgrade $http_upgrade;
|
||||
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
> 如何使用其它数据库?
|
||||
|
||||
在 configs/config.toml 配置文件中,修改 database.dsn
|
||||
|
||||
[推荐] sqlite 应该为本地磁盘路径,建议默认 configs/data.db
|
||||
|
||||
[推荐] postgres 参考格式 `postgres://postgres:123456@127.0.0.1:5432/gb28181?sslmode=disable`
|
||||
|
||||
mysql 参考格式 `mysql://root:123456@127.0.0.1:5432/gb28181?sslmode=disable`
|
||||
|
||||
postgres 和 mysql 的格式即:
|
||||
`<db_type>://<username>:<password>@<ip>:<port>/<db_name>?sslmode=disable`
|
||||
|
||||
> 如何关闭 AI?
|
||||
|
||||
ai 默认是开启状态,1 秒检测 5 帧
|
||||
|
||||
可以在 `configs/config.toml` 中修改 `disabledAI = true` 关闭 ai 检测
|
||||
|
||||
|
||||
|
||||
|
||||
## 文档
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## 日志
|
||||
|
||||
输出位置 `configs/logs/analysis.log`
|
||||
每天一个文件,默认 INFO 级别,支持 debug/info/error 三个级别
|
||||
默认保留 3 天,自动删除旧文件
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: analysis.proto
|
||||
# Protobuf Python Version: 6.31.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
6,
|
||||
31,
|
||||
1,
|
||||
'',
|
||||
'analysis.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61nalysis.proto\x12\x08\x61nalysis\"\xdd\x01\n\x12StartCameraRequest\x12\x11\n\tcamera_id\x18\x01 \x01(\t\x12\x13\n\x0b\x63\x61mera_name\x18\x02 \x01(\t\x12\x10\n\x08rtsp_url\x18\x03 \x01(\t\x12\x12\n\ndetect_fps\x18\x04 \x01(\x05\x12\x0e\n\x06labels\x18\x05 \x03(\t\x12\x11\n\tthreshold\x18\x06 \x01(\x02\x12\x12\n\nroi_points\x18\x07 \x03(\x02\x12\x13\n\x0bretry_limit\x18\x08 \x01(\x05\x12\x14\n\x0c\x63\x61llback_url\x18\n \x01(\t\x12\x17\n\x0f\x63\x61llback_secret\x18\x0b \x01(\t\"x\n\x13StartCameraResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x14\n\x0csource_width\x18\x03 \x01(\x05\x12\x15\n\rsource_height\x18\x04 \x01(\x05\x12\x12\n\nsource_fps\x18\x05 \x01(\x02\"&\n\x11StopCameraRequest\x12\x11\n\tcamera_id\x18\x01 \x01(\t\"6\n\x12StopCameraResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x0f\n\rStatusRequest\"q\n\x0eStatusResponse\x12\x10\n\x08is_ready\x18\x01 \x01(\x08\x12\'\n\x07\x63\x61meras\x18\x02 \x03(\x0b\x32\x16.analysis.CameraStatus\x12$\n\x05stats\x18\x03 \x01(\x0b\x32\x15.analysis.GlobalStats\"t\n\x0c\x43\x61meraStatus\x12\x11\n\tcamera_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12\x18\n\x10\x66rames_processed\x18\x03 \x01(\x03\x12\x12\n\nlast_error\x18\x04 \x01(\t\x12\x13\n\x0bretry_count\x18\x05 \x01(\x05\"W\n\x0bGlobalStats\x12\x16\n\x0e\x61\x63tive_streams\x18\x01 \x01(\x05\x12\x18\n\x10total_detections\x18\x02 \x01(\x03\x12\x16\n\x0euptime_seconds\x18\x03 \x01(\x03\"\x14\n\x12HealthCheckRequest\"\x8e\x01\n\x13HealthCheckResponse\x12;\n\x06status\x18\x01 \x01(\x0e\x32+.analysis.HealthCheckResponse.ServingStatus\":\n\rServingStatus\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SERVING\x10\x01\x12\x0f\n\x0bNOT_SERVING\x10\x02\x32\xe6\x01\n\x0f\x41nalysisService\x12J\n\x0bStartCamera\x12\x1c.analysis.StartCameraRequest\x1a\x1d.analysis.StartCameraResponse\x12G\n\nStopCamera\x12\x1b.analysis.StopCameraRequest\x1a\x1c.analysis.StopCameraResponse\x12>\n\tGetStatus\x12\x17.analysis.StatusRequest\x1a\x18.analysis.StatusResponse2N\n\x06Health\x12\x44\n\x05\x43heck\x12\x1c.analysis.HealthCheckRequest\x1a\x1d.analysis.HealthCheckResponseB\nZ\x08./protosb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'analysis_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'Z\010./protos'
|
||||
_globals['_STARTCAMERAREQUEST']._serialized_start=29
|
||||
_globals['_STARTCAMERAREQUEST']._serialized_end=250
|
||||
_globals['_STARTCAMERARESPONSE']._serialized_start=252
|
||||
_globals['_STARTCAMERARESPONSE']._serialized_end=372
|
||||
_globals['_STOPCAMERAREQUEST']._serialized_start=374
|
||||
_globals['_STOPCAMERAREQUEST']._serialized_end=412
|
||||
_globals['_STOPCAMERARESPONSE']._serialized_start=414
|
||||
_globals['_STOPCAMERARESPONSE']._serialized_end=468
|
||||
_globals['_STATUSREQUEST']._serialized_start=470
|
||||
_globals['_STATUSREQUEST']._serialized_end=485
|
||||
_globals['_STATUSRESPONSE']._serialized_start=487
|
||||
_globals['_STATUSRESPONSE']._serialized_end=600
|
||||
_globals['_CAMERASTATUS']._serialized_start=602
|
||||
_globals['_CAMERASTATUS']._serialized_end=718
|
||||
_globals['_GLOBALSTATS']._serialized_start=720
|
||||
_globals['_GLOBALSTATS']._serialized_end=807
|
||||
_globals['_HEALTHCHECKREQUEST']._serialized_start=809
|
||||
_globals['_HEALTHCHECKREQUEST']._serialized_end=829
|
||||
_globals['_HEALTHCHECKRESPONSE']._serialized_start=832
|
||||
_globals['_HEALTHCHECKRESPONSE']._serialized_end=974
|
||||
_globals['_HEALTHCHECKRESPONSE_SERVINGSTATUS']._serialized_start=916
|
||||
_globals['_HEALTHCHECKRESPONSE_SERVINGSTATUS']._serialized_end=974
|
||||
_globals['_ANALYSISSERVICE']._serialized_start=977
|
||||
_globals['_ANALYSISSERVICE']._serialized_end=1207
|
||||
_globals['_HEALTH']._serialized_start=1209
|
||||
_globals['_HEALTH']._serialized_end=1287
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,116 @@
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class StartCameraRequest(_message.Message):
|
||||
__slots__ = ("camera_id", "camera_name", "rtsp_url", "detect_fps", "labels", "threshold", "roi_points", "retry_limit", "callback_url", "callback_secret")
|
||||
CAMERA_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
CAMERA_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
RTSP_URL_FIELD_NUMBER: _ClassVar[int]
|
||||
DETECT_FPS_FIELD_NUMBER: _ClassVar[int]
|
||||
LABELS_FIELD_NUMBER: _ClassVar[int]
|
||||
THRESHOLD_FIELD_NUMBER: _ClassVar[int]
|
||||
ROI_POINTS_FIELD_NUMBER: _ClassVar[int]
|
||||
RETRY_LIMIT_FIELD_NUMBER: _ClassVar[int]
|
||||
CALLBACK_URL_FIELD_NUMBER: _ClassVar[int]
|
||||
CALLBACK_SECRET_FIELD_NUMBER: _ClassVar[int]
|
||||
camera_id: str
|
||||
camera_name: str
|
||||
rtsp_url: str
|
||||
detect_fps: int
|
||||
labels: _containers.RepeatedScalarFieldContainer[str]
|
||||
threshold: float
|
||||
roi_points: _containers.RepeatedScalarFieldContainer[float]
|
||||
retry_limit: int
|
||||
callback_url: str
|
||||
callback_secret: str
|
||||
def __init__(self, camera_id: _Optional[str] = ..., camera_name: _Optional[str] = ..., rtsp_url: _Optional[str] = ..., detect_fps: _Optional[int] = ..., labels: _Optional[_Iterable[str]] = ..., threshold: _Optional[float] = ..., roi_points: _Optional[_Iterable[float]] = ..., retry_limit: _Optional[int] = ..., callback_url: _Optional[str] = ..., callback_secret: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class StartCameraResponse(_message.Message):
|
||||
__slots__ = ("success", "message", "source_width", "source_height", "source_fps")
|
||||
SUCCESS_FIELD_NUMBER: _ClassVar[int]
|
||||
MESSAGE_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_WIDTH_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_HEIGHT_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_FPS_FIELD_NUMBER: _ClassVar[int]
|
||||
success: bool
|
||||
message: str
|
||||
source_width: int
|
||||
source_height: int
|
||||
source_fps: float
|
||||
def __init__(self, success: bool = ..., message: _Optional[str] = ..., source_width: _Optional[int] = ..., source_height: _Optional[int] = ..., source_fps: _Optional[float] = ...) -> None: ...
|
||||
|
||||
class StopCameraRequest(_message.Message):
|
||||
__slots__ = ("camera_id",)
|
||||
CAMERA_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
camera_id: str
|
||||
def __init__(self, camera_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class StopCameraResponse(_message.Message):
|
||||
__slots__ = ("success", "message")
|
||||
SUCCESS_FIELD_NUMBER: _ClassVar[int]
|
||||
MESSAGE_FIELD_NUMBER: _ClassVar[int]
|
||||
success: bool
|
||||
message: str
|
||||
def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class StatusRequest(_message.Message):
|
||||
__slots__ = ()
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class StatusResponse(_message.Message):
|
||||
__slots__ = ("is_ready", "cameras", "stats")
|
||||
IS_READY_FIELD_NUMBER: _ClassVar[int]
|
||||
CAMERAS_FIELD_NUMBER: _ClassVar[int]
|
||||
STATS_FIELD_NUMBER: _ClassVar[int]
|
||||
is_ready: bool
|
||||
cameras: _containers.RepeatedCompositeFieldContainer[CameraStatus]
|
||||
stats: GlobalStats
|
||||
def __init__(self, is_ready: bool = ..., cameras: _Optional[_Iterable[_Union[CameraStatus, _Mapping]]] = ..., stats: _Optional[_Union[GlobalStats, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class CameraStatus(_message.Message):
|
||||
__slots__ = ("camera_id", "status", "frames_processed", "last_error", "retry_count")
|
||||
CAMERA_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
STATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
FRAMES_PROCESSED_FIELD_NUMBER: _ClassVar[int]
|
||||
LAST_ERROR_FIELD_NUMBER: _ClassVar[int]
|
||||
RETRY_COUNT_FIELD_NUMBER: _ClassVar[int]
|
||||
camera_id: str
|
||||
status: str
|
||||
frames_processed: int
|
||||
last_error: str
|
||||
retry_count: int
|
||||
def __init__(self, camera_id: _Optional[str] = ..., status: _Optional[str] = ..., frames_processed: _Optional[int] = ..., last_error: _Optional[str] = ..., retry_count: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class GlobalStats(_message.Message):
|
||||
__slots__ = ("active_streams", "total_detections", "uptime_seconds")
|
||||
ACTIVE_STREAMS_FIELD_NUMBER: _ClassVar[int]
|
||||
TOTAL_DETECTIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
UPTIME_SECONDS_FIELD_NUMBER: _ClassVar[int]
|
||||
active_streams: int
|
||||
total_detections: int
|
||||
uptime_seconds: int
|
||||
def __init__(self, active_streams: _Optional[int] = ..., total_detections: _Optional[int] = ..., uptime_seconds: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class HealthCheckRequest(_message.Message):
|
||||
__slots__ = ()
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class HealthCheckResponse(_message.Message):
|
||||
__slots__ = ("status",)
|
||||
class ServingStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
UNKNOWN: _ClassVar[HealthCheckResponse.ServingStatus]
|
||||
SERVING: _ClassVar[HealthCheckResponse.ServingStatus]
|
||||
NOT_SERVING: _ClassVar[HealthCheckResponse.ServingStatus]
|
||||
UNKNOWN: HealthCheckResponse.ServingStatus
|
||||
SERVING: HealthCheckResponse.ServingStatus
|
||||
NOT_SERVING: HealthCheckResponse.ServingStatus
|
||||
STATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
status: HealthCheckResponse.ServingStatus
|
||||
def __init__(self, status: _Optional[_Union[HealthCheckResponse.ServingStatus, str]] = ...) -> None: ...
|
||||
@@ -0,0 +1,258 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
import analysis_pb2 as analysis__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.76.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ ' but the generated code in analysis_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class AnalysisServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.StartCamera = channel.unary_unary(
|
||||
'/analysis.AnalysisService/StartCamera',
|
||||
request_serializer=analysis__pb2.StartCameraRequest.SerializeToString,
|
||||
response_deserializer=analysis__pb2.StartCameraResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.StopCamera = channel.unary_unary(
|
||||
'/analysis.AnalysisService/StopCamera',
|
||||
request_serializer=analysis__pb2.StopCameraRequest.SerializeToString,
|
||||
response_deserializer=analysis__pb2.StopCameraResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.GetStatus = channel.unary_unary(
|
||||
'/analysis.AnalysisService/GetStatus',
|
||||
request_serializer=analysis__pb2.StatusRequest.SerializeToString,
|
||||
response_deserializer=analysis__pb2.StatusResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class AnalysisServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def StartCamera(self, request, context):
|
||||
"""启动摄像头分析
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def StopCamera(self, request, context):
|
||||
"""停止摄像头分析
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetStatus(self, request, context):
|
||||
"""获取服务状态
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_AnalysisServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'StartCamera': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.StartCamera,
|
||||
request_deserializer=analysis__pb2.StartCameraRequest.FromString,
|
||||
response_serializer=analysis__pb2.StartCameraResponse.SerializeToString,
|
||||
),
|
||||
'StopCamera': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.StopCamera,
|
||||
request_deserializer=analysis__pb2.StopCameraRequest.FromString,
|
||||
response_serializer=analysis__pb2.StopCameraResponse.SerializeToString,
|
||||
),
|
||||
'GetStatus': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetStatus,
|
||||
request_deserializer=analysis__pb2.StatusRequest.FromString,
|
||||
response_serializer=analysis__pb2.StatusResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'analysis.AnalysisService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('analysis.AnalysisService', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class AnalysisService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def StartCamera(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/analysis.AnalysisService/StartCamera',
|
||||
analysis__pb2.StartCameraRequest.SerializeToString,
|
||||
analysis__pb2.StartCameraResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def StopCamera(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/analysis.AnalysisService/StopCamera',
|
||||
analysis__pb2.StopCameraRequest.SerializeToString,
|
||||
analysis__pb2.StopCameraResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetStatus(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/analysis.AnalysisService/GetStatus',
|
||||
analysis__pb2.StatusRequest.SerializeToString,
|
||||
analysis__pb2.StatusResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class HealthStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.Check = channel.unary_unary(
|
||||
'/analysis.Health/Check',
|
||||
request_serializer=analysis__pb2.HealthCheckRequest.SerializeToString,
|
||||
response_deserializer=analysis__pb2.HealthCheckResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class HealthServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def Check(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_HealthServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'Check': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.Check,
|
||||
request_deserializer=analysis__pb2.HealthCheckRequest.FromString,
|
||||
response_serializer=analysis__pb2.HealthCheckResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'analysis.Health', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('analysis.Health', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class Health(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def Check(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/analysis.Health/Check',
|
||||
analysis__pb2.HealthCheckRequest.SerializeToString,
|
||||
analysis__pb2.HealthCheckResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
@@ -0,0 +1,197 @@
|
||||
import logging
|
||||
from tabnanny import verbose
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from sympy import true
|
||||
from ultralytics import YOLO # type: ignore
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
|
||||
slog = logging.getLogger("Detector")
|
||||
|
||||
|
||||
class ObjectDetector:
|
||||
def __init__(self, model_path: str = "yolo11n.pt", device: str = "auto"):
|
||||
self.model_path = model_path
|
||||
self.device = device
|
||||
self.model: YOLO | None = None
|
||||
self._is_ready = False
|
||||
|
||||
def load_model(self) -> bool:
|
||||
try:
|
||||
slog.info(f"加载模型: {self.model_path} ...")
|
||||
start_time = time.time()
|
||||
self.model = YOLO(self.model_path)
|
||||
if self.device != "auto":
|
||||
self.model.to(self.device)
|
||||
|
||||
# 预热模型
|
||||
dummy_img = np.zeros((640, 640, 3), dtype=np.uint8)
|
||||
self.model.predict(dummy_img, verbose=False)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
slog.info(f"模型加载完成 (耗时: {elapsed:.2f}s)")
|
||||
self._is_ready = True
|
||||
return True
|
||||
except Exception as e:
|
||||
slog.error(f"加载模型失败: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
return self._is_ready and self.model is not None
|
||||
|
||||
def detect(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
threshold: float = 0.5,
|
||||
label_filter: list[str] | None = None,
|
||||
regions: list[tuple[int, int, int, int]] | None = None,
|
||||
) -> tuple[list[dict], float]:
|
||||
|
||||
if not self.is_ready:
|
||||
raise RuntimeError("模型未加载")
|
||||
|
||||
start_time = time.time()
|
||||
detections = []
|
||||
|
||||
if regions and len(regions) > 0:
|
||||
for region in regions:
|
||||
x_min, y_min, x_max, y_max = region
|
||||
h, w = image.shape[:2]
|
||||
x_min = max(0, x_max)
|
||||
y_min = max(0, y_min)
|
||||
x_max = min(w, x_max)
|
||||
y_max = min(h, y_max)
|
||||
|
||||
if x_max <= x_min or y_max <= y_min:
|
||||
continue
|
||||
|
||||
cropped = image[y_min:y_max, x_min:x_max]
|
||||
if cropped.size == 0:
|
||||
continue
|
||||
|
||||
region_detections = self._detect_single(
|
||||
cropped, threshold, label_filter
|
||||
)
|
||||
|
||||
for det in region_detections:
|
||||
det["box"]["x_min"] += x_min
|
||||
det["box"]["y_min"] += y_min
|
||||
det["box"]["x_max"] += x_min
|
||||
det["box"]["y_max"] += y_min
|
||||
detections.append(det)
|
||||
else:
|
||||
detections = self._detect_single(image, threshold, label_filter)
|
||||
|
||||
inference_time_ms = (time.time() - start_time) * 1000
|
||||
return detections, inference_time_ms
|
||||
|
||||
def _detect_single(
|
||||
self, image: np.ndarray, threshold: float, label_filter: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
if not self.model:
|
||||
return []
|
||||
results = self.model.predict(image, conf=threshold, verbose=False)
|
||||
detections = []
|
||||
|
||||
for result in results:
|
||||
if result.boxes is None:
|
||||
continue
|
||||
for box in result.boxes:
|
||||
cls_id = int(box.cls[0])
|
||||
label = self.model.names[cls_id]
|
||||
confidence = float(box.conf[0])
|
||||
|
||||
if label_filter and label not in label_filter:
|
||||
continue
|
||||
x1, y1, x2, y2 = box.xyxy[0].tolist()
|
||||
x_min, y_min = int(x1), int(y1)
|
||||
x_max, y_max = int(x2), int(y2)
|
||||
|
||||
area = (x_max - x_min) * (y_max - y_min)
|
||||
|
||||
detections.append(
|
||||
{
|
||||
"label": label,
|
||||
"confidence": confidence,
|
||||
"box": {
|
||||
"x_min": x_min,
|
||||
"y_min": y_min,
|
||||
"x_max": x_max,
|
||||
"y_max": y_max,
|
||||
},
|
||||
"area": area,
|
||||
"norm_box": {
|
||||
"x": (x1 + x2) / 2 / image.shape[1],
|
||||
"y": (y1 + y2) / 2 / image.shape[0],
|
||||
"w": (x2 - x1) / image.shape[1],
|
||||
"h": (y2 - y1) / image.shape[0],
|
||||
},
|
||||
}
|
||||
)
|
||||
return detections
|
||||
|
||||
|
||||
class MotionDetector:
|
||||
def __init__(self):
|
||||
self.backgrounds: dict[str, np.ndarray] = {}
|
||||
self.motion_threshold = 25
|
||||
self.min_contour_area = 500
|
||||
|
||||
def detect(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
camera_name: str,
|
||||
roi_points: list[float] | None = None,
|
||||
) -> tuple[list[dict[str, Any]], bool]:
|
||||
h, w = image.shape[:2]
|
||||
if len(image.shape) == 3:
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = image.copy()
|
||||
# 模糊可以平滑噪点
|
||||
# (21,21) 模糊大小,必须是奇数
|
||||
gray = cv2.GaussianBlur(gray, (21, 21), 0)
|
||||
|
||||
if camera_name not in self.backgrounds:
|
||||
self.backgrounds[camera_name] = gray.astype(np.float32)
|
||||
return [], False
|
||||
|
||||
cv2.accumulateWeighted(gray, self.backgrounds[camera_name], 0.1)
|
||||
|
||||
frame_delta = cv2.absdiff(
|
||||
gray, cv2.convertScaleAbs(self.backgrounds[camera_name])
|
||||
)
|
||||
thresh = cv2.threshold(
|
||||
frame_delta, self.motion_threshold, 255, cv2.THRESH_BINARY
|
||||
)[1]
|
||||
|
||||
if roi_points and len(roi_points) > 0:
|
||||
mask = np.zeros((h, w), dtype=np.uint8)
|
||||
|
||||
pts = []
|
||||
for i in range(0, len(roi_points), 2):
|
||||
pts.append((int(roi_points[i] * w), int(roi_points[i + 1] * h)))
|
||||
pts_np = np.array([pts], dtype=np.int32)
|
||||
cv2.fillPoly(mask, pts_np, 255)
|
||||
thresh = cv2.bitwise_and(thresh, mask=mask)
|
||||
|
||||
thresh = cv2.dilate(thresh, None, iterations=2)
|
||||
|
||||
contours, _ = cv2.findContours(
|
||||
thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
||||
)
|
||||
|
||||
motion_boxes = []
|
||||
for contour in contours:
|
||||
if cv2.contourArea(contour) < self.min_contour_area:
|
||||
continue
|
||||
x, y, w, h = cv2.boundingRect(contour)
|
||||
motion_boxes.append(
|
||||
{"y_min": y, "x_min": x, "y_max": y + h, "x_max": x + w}
|
||||
)
|
||||
has_motion = len(motion_boxes) > 0
|
||||
return motion_boxes, has_motion
|
||||
@@ -0,0 +1,227 @@
|
||||
from collections import deque
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Deque, Optional
|
||||
import numpy as np
|
||||
|
||||
|
||||
slog = logging.getLogger("Capture")
|
||||
|
||||
|
||||
class LogPipe(threading.Thread):
|
||||
def __init__(self, log_name: str):
|
||||
super().__init__(daemon=True)
|
||||
self.logger = logging.getLogger(log_name)
|
||||
self.deque: Deque[str] = deque(maxlen=100)
|
||||
self.fd_read, self.fd_write = os.pipe()
|
||||
self.pipe_reader = os.fdopen(self.fd_read)
|
||||
self.start()
|
||||
|
||||
def fileno(self):
|
||||
return self.fd_write
|
||||
|
||||
def run(self):
|
||||
# 使用 iter() 包装 self.pipe_reader.readline 方法和空字符串""作为哨兵,使其不断读取管道内容。
|
||||
# iter(self.pipe_reader.readline, "") 会不断调用 readline(),直到返回空字符串(代表 EOF),循环终止。
|
||||
for line in iter(self.pipe_reader.readline, ""):
|
||||
self.deque.append(line)
|
||||
self.pipe_reader.close()
|
||||
|
||||
def dump(self):
|
||||
while len(self.deque) > 0:
|
||||
self.logger.error(self.deque.popleft())
|
||||
|
||||
def close(self):
|
||||
os.close(self.fd_read)
|
||||
|
||||
|
||||
class FrameCapture:
|
||||
def __init__(self, rtsp_url: str, output_queue: queue.Queue, detect_fps: int = 5):
|
||||
self.rtsp_url = rtsp_url
|
||||
self.output_queue = output_queue
|
||||
self.target_fps = detect_fps
|
||||
self._stop_event = threading.Event()
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._proccess: Optional[subprocess.Popen] = None
|
||||
|
||||
# 流信息
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.fps = 0.0
|
||||
|
||||
def start(self):
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
return
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
|
||||
self._thread.start()
|
||||
slog.info(f"FrameCapture started for {self.rtsp_url}")
|
||||
|
||||
def stop(self):
|
||||
# 设置停止事件
|
||||
self._stop_event.set()
|
||||
# 终止进程
|
||||
self._terminate_process()
|
||||
# 等待线程结束
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2)
|
||||
slog.info(f"FrameCapture stopped for {self.rtsp_url}")
|
||||
|
||||
def _get_stream_info(self) -> bool:
|
||||
slog.debug(f"正在探测流信息... {self.rtsp_url}")
|
||||
ffprobe_cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height,r_frame_rate",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"-rtsp_transport",
|
||||
"tcp", # 强制 TCP 更稳定
|
||||
self.rtsp_url,
|
||||
]
|
||||
try:
|
||||
# 执行一个外部命令(比如系统命令、shell 脚本、其他可执行程序),并直接返回该命令在标准输出(stdout)中打印的内容。
|
||||
output = (
|
||||
subprocess.check_output(ffprobe_cmd, timeout=15).decode("utf-8").strip()
|
||||
)
|
||||
parts = output.split(",")
|
||||
if len(parts) >= 2:
|
||||
self.width = int(parts[0])
|
||||
self.height = int(parts[1])
|
||||
if len(parts) >= 3 and "/" in parts[2]:
|
||||
num, den = parts[2].split("/")
|
||||
self.fps = float(num) / float(den)
|
||||
else:
|
||||
self.fps = 25.0
|
||||
slog.info(
|
||||
f"ffprobe 探测成功: {self.width}x{self.height} @ {self.fps:.2f}fps"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
slog.error(f"探测流信息失败: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def _capture_loop(self):
|
||||
|
||||
log_pipe: Optional[LogPipe] = None
|
||||
while not self._stop_event.is_set():
|
||||
if self.width == 0 or self.height == 0:
|
||||
if not self._get_stream_info():
|
||||
time.sleep(3)
|
||||
continue
|
||||
if log_pipe:
|
||||
log_pipe.close()
|
||||
log_pipe = LogPipe(f"ffmpeg.{self.rtsp_url}")
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"warning", # 只输出 warning 以上,减少 IO
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
"-i",
|
||||
self.rtsp_url,
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"bgr24", # 直接输出 OpenCV 友好的 BGR 格式
|
||||
"-r",
|
||||
str(self.target_fps), # 降低帧率
|
||||
"pipe:1",
|
||||
]
|
||||
slog.info(f"启动 ffmpeg 进程: {' '.join(ffmpeg_cmd[:-2])} ...")
|
||||
|
||||
try:
|
||||
self._proccess = subprocess.Popen(
|
||||
ffmpeg_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=log_pipe.fileno(),
|
||||
bufsize=10**7,
|
||||
)
|
||||
except Exception as e:
|
||||
slog.error(f"启动 ffmpeg 进程失败: {e}")
|
||||
if log_pipe:
|
||||
log_pipe.dump()
|
||||
time.sleep(3)
|
||||
continue
|
||||
frame_size = self.width * self.height * 3
|
||||
slog.info(f"开始读取帧 (size={frame_size})...")
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
if self._proccess.poll() is not None:
|
||||
slog.error("FFmpeg 进程意外退出")
|
||||
log_pipe.dump()
|
||||
break
|
||||
if self._proccess.stdout is None:
|
||||
slog.error("FFmpeg 进程 stdout 为空")
|
||||
break
|
||||
raw_bytes = self._proccess.stdout.read(frame_size)
|
||||
|
||||
if len(raw_bytes) != frame_size:
|
||||
slog.warning("读取到不完整的帧 (流中断?)")
|
||||
log_pipe.dump() # 可能有网络错误
|
||||
break
|
||||
image = np.frombuffer(raw_bytes, dtype=np.uint8).reshape(
|
||||
self.height, self.width, 3
|
||||
)
|
||||
|
||||
try:
|
||||
while not self.output_queue.empty():
|
||||
try:
|
||||
self.output_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
self.output_queue.put_nowait(image)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
slog.error(f"读取帧失败: {e}")
|
||||
log_pipe.dump()
|
||||
break
|
||||
self._terminate_process()
|
||||
|
||||
if log_pipe:
|
||||
log_pipe.close()
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
def _terminate_process(self):
|
||||
if self._proccess:
|
||||
if self._proccess.poll() is None:
|
||||
self._proccess.terminate()
|
||||
try:
|
||||
self._proccess.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._proccess.kill()
|
||||
self._proccess = None
|
||||
|
||||
def _hide_password(self, url):
|
||||
"""隐藏 URL 中的密码"""
|
||||
try:
|
||||
if "@" in url:
|
||||
parts = url.split("@")
|
||||
if "//" in parts[0]:
|
||||
protocol_auth = parts[0].split("//")
|
||||
if ":" in protocol_auth[1]:
|
||||
user = protocol_auth[1].split(":")[0]
|
||||
return f"{protocol_auth[0]}//{user}:***@{parts[1]}"
|
||||
return url
|
||||
except:
|
||||
return url
|
||||
|
||||
def get_stream_info(self):
|
||||
"""返回流的基本信息"""
|
||||
return self.width, self.height, self.fps
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
统一日志管理模块
|
||||
功能:
|
||||
1. 根据命令行参数设置日志级别。
|
||||
2. 支持日志文件按天轮转 (Daily Rotation)。
|
||||
3. 支持自动清理旧日志 (Retention)。
|
||||
4. 同时输出到控制台 (Console) 和文件 (File)。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
LOG_DIR = "../configs/logs"
|
||||
LOG_FILE = "analysis.log"
|
||||
|
||||
|
||||
def setup_logging(level_str: str = "INFO", retention_days: int = 3):
|
||||
# 1. 转换级别
|
||||
level = getattr(logging, level_str.upper(), logging.INFO)
|
||||
|
||||
# 2. 确保日志目录存在
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
|
||||
log_path = os.path.join(LOG_DIR, LOG_FILE)
|
||||
|
||||
# 3. 创建 Root Logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
|
||||
# 清除已有的 handlers (避免重复打印)
|
||||
root_logger.handlers = []
|
||||
|
||||
# 4. 格式化器
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(levelname)-8s | %(process)d:%(threadName)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
# 5. Handler 1: 控制台输出
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 6. Handler 2: 文件输出 (按天轮转)
|
||||
# TimedRotatingFileHandler:
|
||||
# - when='midnight': 每天午夜切分
|
||||
# - interval=1: 每1天
|
||||
# - backupCount=retention_days: 保留几天,超出的会被删除
|
||||
# - encoding='utf-8': 防止中文乱码
|
||||
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
filename=log_path,
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=retention_days,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# 设置后缀格式,例如 app.log.2023-12-31
|
||||
file_handler.suffix = "%Y-%m-%d"
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# 打印一条初始化日志
|
||||
logging.info(
|
||||
f"日志系统已初始化: Level={level_str}, Path={log_path}, Retention={retention_days} days"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import logging
|
||||
|
||||
from logger import setup_logging
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_logging()
|
||||
logging.info("test")
|
||||
@@ -0,0 +1,509 @@
|
||||
import os
|
||||
import signal
|
||||
|
||||
# 解决 macOS 上 OpenMP 库冲突问题,必须在导入 torch/cv2 等库之前设置
|
||||
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from concurrent import futures
|
||||
from concurrent.futures import thread
|
||||
import logging
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
import requests
|
||||
|
||||
import grpc
|
||||
from torch.export.exported_program import PassType
|
||||
|
||||
# 添加当前目录到 path 以支持直接运行
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import logger
|
||||
from detect import MotionDetector, ObjectDetector
|
||||
from frame_capture import FrameCapture
|
||||
import cv2
|
||||
|
||||
# 导入生成的 proto 代码
|
||||
try:
|
||||
import analysis_pb2
|
||||
import analysis_pb2_grpc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
slog = logging.getLogger("AI")
|
||||
|
||||
# 全局配置
|
||||
GLOBAL_CONFIG = {
|
||||
"callback_url": "",
|
||||
"callback_secret": "",
|
||||
}
|
||||
|
||||
# 保存父进程 PID,用于检测父进程是否退出
|
||||
_PARENT_PID = os.getppid()
|
||||
|
||||
|
||||
def _watch_parent_process():
|
||||
"""
|
||||
监控父进程是否存活。当 Go 父进程退出后,Python 子进程应该自动退出,
|
||||
避免成为孤儿进程持续占用端口和资源。
|
||||
"""
|
||||
while True:
|
||||
time.sleep(3)
|
||||
# 检查父进程是否还存在
|
||||
# 如果父进程退出,当前进程的 ppid 会变成 1 (init/launchd) 或其他进程
|
||||
current_ppid = os.getppid()
|
||||
if current_ppid != _PARENT_PID:
|
||||
slog.warning(
|
||||
f"父进程已退出 (原 PID: {_PARENT_PID}, 当前 PPID: {current_ppid}),Python 进程退出"
|
||||
)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
class CameraTask:
|
||||
def __init__(
|
||||
self,
|
||||
camera_id: str,
|
||||
rtsp_url: str,
|
||||
config: dict[str, Any],
|
||||
detector: ObjectDetector,
|
||||
motion_detector: MotionDetector,
|
||||
) -> None:
|
||||
self.camera_id = camera_id
|
||||
self.rtsp_url = rtsp_url
|
||||
self.config = config
|
||||
self.detector = detector
|
||||
self.motion_detector = motion_detector
|
||||
|
||||
self.status = "initializing"
|
||||
self.frames_processed = 0
|
||||
self.retry_count = 0
|
||||
self.last_error = ""
|
||||
self._stop_event = threading.Event()
|
||||
self._thread: threading.Thread | None = None
|
||||
|
||||
self.frame_queue = queue.Queue(maxsize=1)
|
||||
self.capture = FrameCapture(
|
||||
rtsp_url, self.frame_queue, config.get("detect_fps", 5)
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self.status = "running"
|
||||
self.capture.start()
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(target=self._analysis_loop, daemon=True)
|
||||
self._thread.start()
|
||||
slog.info(f"CameraTask started for {self.camera_id}")
|
||||
|
||||
def stop(self):
|
||||
self.status = "stopping"
|
||||
self._stop_event.set()
|
||||
self.capture.stop()
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2)
|
||||
slog.info(f"CameraTask stopped for {self.camera_id}")
|
||||
|
||||
def _analysis_loop(self):
|
||||
error_streak = 0
|
||||
retry_limit = int(self.config.get("retry_limit", 10))
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
try:
|
||||
frame = self.frame_queue.get(timeout=2.0)
|
||||
except queue.Empty:
|
||||
slog.debug("CameraTask frame queue empty, skipping")
|
||||
continue
|
||||
|
||||
error_streak = 0
|
||||
self.frames_processed += 1
|
||||
|
||||
roi_points = self.config.get("roi_points")
|
||||
motion_boxes, has_motion = self.motion_detector.detect(
|
||||
frame, self.camera_id, roi_points
|
||||
)
|
||||
|
||||
if not has_motion:
|
||||
continue
|
||||
|
||||
try:
|
||||
labels = self.config.get("labels")
|
||||
if labels and isinstance(labels, list):
|
||||
safe_labels = [str(l) for l in labels]
|
||||
else:
|
||||
safe_labels = None
|
||||
|
||||
detections, _ = self.detector.detect(
|
||||
frame,
|
||||
threshold=self.config.get("threshold", 0.5),
|
||||
label_filter=safe_labels,
|
||||
# 暂时只支持全图检测,未来优化可以只检测 motion_boxes 区域
|
||||
regions=None,
|
||||
)
|
||||
except Exception as e:
|
||||
slog.error(f"CameraTask labels error: {e}")
|
||||
continue
|
||||
|
||||
if not detections:
|
||||
continue
|
||||
self._send_detection_callback(detections, frame)
|
||||
except Exception as e:
|
||||
slog.error(f"CameraTask analysis loop error: {e}")
|
||||
error_streak += 1
|
||||
self.last_error = str(e)
|
||||
if error_streak >= retry_limit:
|
||||
self.status = "error"
|
||||
self._send_stopped_callback("error", self.last_error)
|
||||
self.capture.stop()
|
||||
break
|
||||
# 防止 cpu 在异常里空转
|
||||
time.sleep(1)
|
||||
|
||||
def _send_detection_callback(self, detections, frame):
|
||||
timestamp = int(time.time() * 1000)
|
||||
draw_frame = frame.copy()
|
||||
for det in detections:
|
||||
box = det["box"]
|
||||
label = f"{det['label']} {det['confidence']:.2f}"
|
||||
|
||||
# 坐标
|
||||
p1 = (box["x_min"], box["y_min"])
|
||||
p2 = (box["x_max"], box["y_max"])
|
||||
|
||||
# 画矩形框 (红色,线宽2)
|
||||
cv2.rectangle(draw_frame, p1, p2, (0, 0, 255), 2)
|
||||
|
||||
# 画文字背景条,防止文字看不清
|
||||
t_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0]
|
||||
p2_text = (p1[0] + t_size[0], p1[1] - t_size[1] - 3)
|
||||
cv2.rectangle(draw_frame, p1, p2_text, (0, 0, 255), -1) # -1 表示实心填充
|
||||
|
||||
# 画文字 (白色)
|
||||
cv2.putText(
|
||||
draw_frame,
|
||||
label,
|
||||
(p1[0], p1[1] - 2),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(255, 255, 255),
|
||||
1,
|
||||
)
|
||||
success, buffer = cv2.imencode(".jpg", draw_frame)
|
||||
snapshot_b64 = ""
|
||||
if success:
|
||||
snapshot_b64 = base64.b64encode(buffer).decode("utf-8")
|
||||
|
||||
payload = {
|
||||
"camera_id": self.camera_id,
|
||||
"timestamp": timestamp,
|
||||
"detections": detections,
|
||||
"snapshot": snapshot_b64,
|
||||
"snapshot_width": frame.shape[1],
|
||||
"snapshot_height": frame.shape[0],
|
||||
}
|
||||
|
||||
send_callback(self.config, "/events", payload)
|
||||
|
||||
def _send_stopped_callback(self, reason, message):
|
||||
payload = {
|
||||
"camera_id": self.camera_id,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"reason": reason,
|
||||
"message": message,
|
||||
}
|
||||
send_callback(self.config, "/stopped", payload)
|
||||
|
||||
|
||||
class HealthServicer(analysis_pb2_grpc.HealthServicer):
|
||||
def __init__(self, servicer):
|
||||
self._servicer = servicer
|
||||
|
||||
def Check(self, request, context):
|
||||
if not self._servicer.is_ready:
|
||||
return analysis_pb2.HealthCheckResponse(
|
||||
status=analysis_pb2.HealthCheckResponse.NOT_SERVING
|
||||
)
|
||||
return analysis_pb2.HealthCheckResponse(
|
||||
status=analysis_pb2.HealthCheckResponse.SERVING
|
||||
)
|
||||
|
||||
|
||||
class AnalysisServiceServicer(analysis_pb2_grpc.AnalysisServiceServicer):
|
||||
def __init__(self, model_path):
|
||||
self._camera_tasks: dict[str, CameraTask] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._is_ready = False
|
||||
self._start_time = time.time()
|
||||
|
||||
self.object_detector = ObjectDetector(model_path)
|
||||
self.motion_detector = MotionDetector()
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
return self._is_ready
|
||||
|
||||
def initialize(self):
|
||||
slog.info("AnalysisService initializing...")
|
||||
success = self.object_detector.load_model()
|
||||
self._is_ready = success
|
||||
|
||||
if not success:
|
||||
slog.error("AnalysisService initialization failed")
|
||||
return
|
||||
slog.info("AnalysisService initialized")
|
||||
threading.Thread(target=send_started_callback).start()
|
||||
|
||||
def StartCamera(self, request, context):
|
||||
if not self._is_ready:
|
||||
context.set_details("model loadding")
|
||||
context.set_code(grpc.StatusCode.UNAVAILABLE)
|
||||
return analysis_pb2.StartCameraResponse(
|
||||
success=False, message="model loadding"
|
||||
)
|
||||
camera_id = request.camera_id
|
||||
with self._lock:
|
||||
if camera_id in self._camera_tasks:
|
||||
slog.info(
|
||||
f"Camera {camera_id} already exists, status: {self._camera_tasks[camera_id].status}"
|
||||
)
|
||||
return analysis_pb2.StartCameraResponse(
|
||||
success=True, message="任务已运行"
|
||||
)
|
||||
cb_url = request.callback_url or GLOBAL_CONFIG["callback_url"]
|
||||
cb_secret = request.callback_secret or GLOBAL_CONFIG["callback_secret"]
|
||||
if not cb_url:
|
||||
context.set_details("callback url is required")
|
||||
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
|
||||
return analysis_pb2.StartCameraResponse(
|
||||
success=False, message="callback url is required"
|
||||
)
|
||||
config = {
|
||||
"detect_fps": request.detect_fps,
|
||||
"labels": list(request.labels),
|
||||
"threshold": request.threshold,
|
||||
"roi_points": list(request.roi_points),
|
||||
"retry_limit": request.retry_limit,
|
||||
"callback_url": cb_url,
|
||||
"callback_secret": cb_secret,
|
||||
}
|
||||
|
||||
task = CameraTask(
|
||||
camera_id,
|
||||
rtsp_url=request.rtsp_url,
|
||||
config=config,
|
||||
detector=self.object_detector,
|
||||
motion_detector=self.motion_detector,
|
||||
)
|
||||
task.start()
|
||||
self._camera_tasks[camera_id] = task
|
||||
|
||||
timeout = 5.0
|
||||
start = time.time()
|
||||
w, h, fps = 0, 0, 0.0
|
||||
while time.time() - start < timeout:
|
||||
w, h, fps = task.capture.get_stream_info()
|
||||
if w > 0:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
return analysis_pb2.StartCameraResponse(
|
||||
success=True,
|
||||
message="任务已启动",
|
||||
source_width=w,
|
||||
source_height=h,
|
||||
source_fps=fps,
|
||||
)
|
||||
|
||||
def StopCamera(self, request, context):
|
||||
camera_id = request.camera_id
|
||||
with self._lock:
|
||||
if camera_id not in self._camera_tasks:
|
||||
return analysis_pb2.StopCameraResponse(
|
||||
success=False, message="Camera not found"
|
||||
)
|
||||
|
||||
task = self._camera_tasks.pop(camera_id)
|
||||
task.stop()
|
||||
return analysis_pb2.StopCameraResponse(success=True, message="任务已停止")
|
||||
|
||||
def GetStatus(self, request, context):
|
||||
response = analysis_pb2.StatusResponse()
|
||||
response.is_ready = self._is_ready
|
||||
response.stats.active_streams = len(self._camera_tasks)
|
||||
response.stats.uptime_seconds = int(time.time() - self._start_time)
|
||||
|
||||
with self._lock:
|
||||
for cid, task in self._camera_tasks.items():
|
||||
cam_status = analysis_pb2.CameraStatus(
|
||||
camera_id=cid,
|
||||
status=task.status,
|
||||
frames_processed=task.frames_processed,
|
||||
retry_count=task.retry_count,
|
||||
last_error=task.last_error,
|
||||
)
|
||||
response.cameras.append(cam_status)
|
||||
return response
|
||||
|
||||
|
||||
def send_callback(config: dict, path: str, payload: dict):
|
||||
"""
|
||||
发送回调到指定路径,路径会拼接到 callback_url 后面。
|
||||
例如: callback_url=http://127.0.0.1:15123, path=/events
|
||||
最终请求: POST http://127.0.0.1:15123/events
|
||||
"""
|
||||
url = config.get("callback_url", "")
|
||||
secret = config.get("callback_secret", "")
|
||||
if not url:
|
||||
return
|
||||
|
||||
full_url = url.rstrip("/") + path
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if secret:
|
||||
headers["Authorization"] = secret
|
||||
|
||||
try:
|
||||
threading.Thread(
|
||||
target=requests.post,
|
||||
args=(full_url,),
|
||||
kwargs={
|
||||
"json": payload,
|
||||
"headers": headers,
|
||||
"timeout": 5.0,
|
||||
},
|
||||
).start()
|
||||
except Exception as e:
|
||||
slog.error(f"Failed to send callback to {path}: {e}")
|
||||
|
||||
|
||||
def send_started_callback():
|
||||
"""
|
||||
向 Go 服务发送启动通知,用于确认 Python 进程与 Go 服务的连接是否正常。
|
||||
如果 Go 服务返回 404,说明回调接口不存在,Python 进程应该退出,避免成为孤儿进程。
|
||||
"""
|
||||
url = GLOBAL_CONFIG["callback_url"]
|
||||
secret = GLOBAL_CONFIG["callback_secret"]
|
||||
if not url:
|
||||
return
|
||||
|
||||
full_url = url.rstrip("/") + "/started"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if secret:
|
||||
headers["Authorization"] = secret
|
||||
|
||||
payload = {
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"message": "AI Analysis Service Started",
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
retry_interval = 2
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
slog.info(f"Sending started callback (attempt {attempt}/{max_retries})...")
|
||||
try:
|
||||
resp = requests.post(full_url, json=payload, headers=headers, timeout=5)
|
||||
if resp.status_code == 404 and attempt == max_retries - 1:
|
||||
slog.error(f"回调接口返回 404,Go 服务可能已停止,退出 Python 进程")
|
||||
os._exit(1)
|
||||
if resp.ok:
|
||||
slog.info("启动通知发送成功")
|
||||
return
|
||||
slog.warning(f"启动通知返回非成功状态: {resp.status_code}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
slog.warning(f"发送启动通知失败 (连接错误): {e}")
|
||||
except Exception as e:
|
||||
slog.error(f"发送启动通知失败: {e}")
|
||||
|
||||
if attempt < max_retries:
|
||||
time.sleep(retry_interval)
|
||||
|
||||
slog.error(f"启动通知发送失败,已重试 {max_retries} 次")
|
||||
|
||||
|
||||
def send_keepalive_callback(stats: dict):
|
||||
"""
|
||||
发送心跳回调,用于定期向 Go 服务报告 AI 服务状态。
|
||||
"""
|
||||
url = GLOBAL_CONFIG["callback_url"]
|
||||
secret = GLOBAL_CONFIG["callback_secret"]
|
||||
if not url:
|
||||
return
|
||||
|
||||
full_url = url.rstrip("/") + "/keepalive"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if secret:
|
||||
headers["Authorization"] = secret
|
||||
|
||||
payload = {
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"stats": stats,
|
||||
"message": "Service running normally",
|
||||
}
|
||||
|
||||
try:
|
||||
requests.post(full_url, json=payload, headers=headers, timeout=5)
|
||||
except Exception as e:
|
||||
slog.debug(f"Failed to send keepalive callback: {e}")
|
||||
|
||||
|
||||
def serve(port, model_path):
|
||||
if "analysis_pb2_grpc" not in sys.modules:
|
||||
slog.error("Proto 代码未加载,退出。")
|
||||
return
|
||||
|
||||
# 启动父进程监控线程,确保 Go 退出时 Python 也退出
|
||||
threading.Thread(target=_watch_parent_process, daemon=True).start()
|
||||
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=20))
|
||||
servicer = AnalysisServiceServicer(model_path)
|
||||
analysis_pb2_grpc.add_AnalysisServiceServicer_to_server(servicer, server)
|
||||
|
||||
health_servicer = HealthServicer(servicer)
|
||||
analysis_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
|
||||
|
||||
server.add_insecure_port(f"[::]:{port}")
|
||||
server.start()
|
||||
slog.info(f"AnalysisService started: 0.0.0.0:{port}")
|
||||
|
||||
threading.Thread(target=servicer.initialize).start()
|
||||
|
||||
try:
|
||||
server.wait_for_termination()
|
||||
except KeyboardInterrupt:
|
||||
server.stop(0)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", type=int, default=50051)
|
||||
parser.add_argument("--model", type=str, default="yolo11n.pt")
|
||||
parser.add_argument(
|
||||
"--callback-url",
|
||||
type=str,
|
||||
default="http://127.0.0.1:15123",
|
||||
help="回调基础URL,各回调路由会自动拼接",
|
||||
)
|
||||
parser.add_argument("--callback-secret", type=str, default="", help="回调秘钥")
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
type=str,
|
||||
default="INFO",
|
||||
help="日志级别 (DEBUG/INFO/ERROR)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
logger.setup_logging(level_str=args.log_level)
|
||||
|
||||
GLOBAL_CONFIG["callback_url"] = args.callback_url
|
||||
GLOBAL_CONFIG["callback_secret"] = args.callback_secret
|
||||
|
||||
slog.debug(
|
||||
f"log level: {args.log_level}, model: {args.model}, callback url: {args.callback_url}, callback secret: {args.callback_secret}"
|
||||
)
|
||||
|
||||
serve(args.port, args.model)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
# gRPC 框架
|
||||
grpcio==1.76.0
|
||||
grpcio-tools==1.76.0
|
||||
|
||||
# 图像处理
|
||||
opencv-python-headless>=4.8.0
|
||||
numpy>=2.3.5
|
||||
|
||||
# YOLO 模型
|
||||
ultralytics>=8.0.0
|
||||
|
||||
# 科学计算
|
||||
scipy>=1.16.3
|
||||
|
||||
# HTTP 客户端 (用于回调)
|
||||
requests>=2.31.0
|
||||
@@ -0,0 +1,281 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: GOWVP AI 分析服务回调 API
|
||||
version: 2.0.0
|
||||
description: |
|
||||
AI 分析服务发出的 Webhook 回调 API 规范。
|
||||
主服务 (Go) 或其他客户端应实现此 API 以接收 AI 事件。
|
||||
|
||||
回调地址格式: `{callback_url}/{path}`
|
||||
例如: callback_url=http://127.0.0.1:15123,则心跳回调为 http://127.0.0.1:15123/keepalive
|
||||
|
||||
servers:
|
||||
- url: "{callback_url}"
|
||||
description: AI 回调服务地址
|
||||
variables:
|
||||
callback_url:
|
||||
default: http://127.0.0.1:15123
|
||||
description: 回调基础 URL
|
||||
|
||||
paths:
|
||||
/keepalive:
|
||||
post:
|
||||
summary: 心跳回调
|
||||
description: |
|
||||
AI 分析服务定期发送心跳,用于确认服务存活状态。
|
||||
主服务可以通过此接口监控 AI 服务的运行状态。
|
||||
security:
|
||||
- basicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/KeepaliveEvent"
|
||||
example:
|
||||
timestamp: 1678886400000
|
||||
stats:
|
||||
active_streams: 5
|
||||
total_detections: 1200
|
||||
uptime_seconds: 3600
|
||||
message: "Service running normally"
|
||||
responses:
|
||||
"200":
|
||||
description: 成功接收心跳
|
||||
"401":
|
||||
description: 未授权 (认证失败)
|
||||
|
||||
/started:
|
||||
post:
|
||||
summary: 服务启动回调
|
||||
description: |
|
||||
AI 分析服务启动完成后发送此回调,用于确认服务已就绪。
|
||||
主服务收到此回调后可以开始向 AI 服务发送分析任务。
|
||||
security:
|
||||
- basicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StartedEvent"
|
||||
example:
|
||||
timestamp: 1678886400000
|
||||
message: "AI Analysis Service Started"
|
||||
responses:
|
||||
"200":
|
||||
description: 成功接收启动通知
|
||||
"401":
|
||||
description: 未授权 (认证失败)
|
||||
|
||||
/events:
|
||||
post:
|
||||
summary: 检测事件回调
|
||||
description: |
|
||||
当 AI 检测到目标对象时发送此回调。
|
||||
包含检测结果和带有标注的快照图片。
|
||||
security:
|
||||
- basicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DetectionEvent"
|
||||
example:
|
||||
camera_id: "cam_office_01"
|
||||
timestamp: 1678886400000
|
||||
detections:
|
||||
- label: "person"
|
||||
confidence: 0.95
|
||||
box: { x_min: 100, y_min: 100, x_max: 200, y_max: 300 }
|
||||
area: 20000
|
||||
snapshot: "/9j/4AAQSkZJRg..."
|
||||
snapshot_width: 1920
|
||||
snapshot_height: 1080
|
||||
responses:
|
||||
"200":
|
||||
description: 成功接收检测事件
|
||||
"401":
|
||||
description: 未授权 (认证失败)
|
||||
"400":
|
||||
description: 数据格式无效
|
||||
|
||||
/stopped:
|
||||
post:
|
||||
summary: 任务停止回调
|
||||
description: |
|
||||
当某个摄像头的分析任务停止时发送此回调。
|
||||
可能是用户主动停止,也可能是因为错误导致任务终止。
|
||||
security:
|
||||
- basicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StoppedEvent"
|
||||
example:
|
||||
camera_id: "cam_office_01"
|
||||
timestamp: 1678886400000
|
||||
reason: "error"
|
||||
message: "RTSP stream connection timed out"
|
||||
responses:
|
||||
"200":
|
||||
description: 成功接收停止通知
|
||||
"401":
|
||||
description: 未授权 (认证失败)
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
使用用户名 'gowvp' 和配置的密钥 (secret) 进行 Basic Authentication。
|
||||
示例 Header: `Authorization: Basic Z293dnA6YOUR_SECRET`
|
||||
|
||||
schemas:
|
||||
KeepaliveEvent:
|
||||
type: object
|
||||
required:
|
||||
- timestamp
|
||||
properties:
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 事件发生的 Unix 时间戳 (毫秒)。
|
||||
stats:
|
||||
$ref: "#/components/schemas/GlobalStats"
|
||||
message:
|
||||
type: string
|
||||
description: 附加消息。
|
||||
|
||||
StartedEvent:
|
||||
type: object
|
||||
required:
|
||||
- timestamp
|
||||
properties:
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 服务启动的 Unix 时间戳 (毫秒)。
|
||||
message:
|
||||
type: string
|
||||
description: 启动消息。
|
||||
|
||||
DetectionEvent:
|
||||
type: object
|
||||
required:
|
||||
- camera_id
|
||||
- timestamp
|
||||
- detections
|
||||
properties:
|
||||
camera_id:
|
||||
type: string
|
||||
description: 生成事件的摄像头 ID。
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 检测发生的 Unix 时间戳 (毫秒)。
|
||||
detections:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/DetectionObject"
|
||||
snapshot:
|
||||
type: string
|
||||
description: 带有检测标注的帧画面 Base64 编码 (JPEG 格式)。
|
||||
snapshot_width:
|
||||
type: integer
|
||||
description: 快照图片的宽度。
|
||||
snapshot_height:
|
||||
type: integer
|
||||
description: 快照图片的高度。
|
||||
|
||||
StoppedEvent:
|
||||
type: object
|
||||
required:
|
||||
- camera_id
|
||||
- timestamp
|
||||
- reason
|
||||
properties:
|
||||
camera_id:
|
||||
type: string
|
||||
description: 停止任务的摄像头 ID。
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 停止发生的 Unix 时间戳 (毫秒)。
|
||||
reason:
|
||||
type: string
|
||||
description: 停止原因 (例如 'user_requested', 'error')。
|
||||
message:
|
||||
type: string
|
||||
description: 包含具体原因的详细信息。
|
||||
|
||||
DetectionObject:
|
||||
type: object
|
||||
required:
|
||||
- label
|
||||
- confidence
|
||||
- box
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
description: 检测到的物体类别 (例如 'person', 'car')。
|
||||
confidence:
|
||||
type: number
|
||||
format: float
|
||||
description: 置信度分数 (0.0 - 1.0)。
|
||||
box:
|
||||
$ref: "#/components/schemas/BoundingBox"
|
||||
area:
|
||||
type: integer
|
||||
description: 边界框的像素面积。
|
||||
norm_box:
|
||||
$ref: "#/components/schemas/NormalizedBoundingBox"
|
||||
|
||||
BoundingBox:
|
||||
type: object
|
||||
description: 像素坐标边界框
|
||||
properties:
|
||||
x_min:
|
||||
type: integer
|
||||
y_min:
|
||||
type: integer
|
||||
x_max:
|
||||
type: integer
|
||||
y_max:
|
||||
type: integer
|
||||
|
||||
NormalizedBoundingBox:
|
||||
type: object
|
||||
description: 归一化边界框 (0.0-1.0 范围,相对于图像宽高)
|
||||
properties:
|
||||
x:
|
||||
type: number
|
||||
description: 中心点 X 坐标
|
||||
y:
|
||||
type: number
|
||||
description: 中心点 Y 坐标
|
||||
w:
|
||||
type: number
|
||||
description: 宽度
|
||||
h:
|
||||
type: number
|
||||
description: 高度
|
||||
|
||||
GlobalStats:
|
||||
type: object
|
||||
properties:
|
||||
active_streams:
|
||||
type: integer
|
||||
description: 当前活跃的 RTSP 流数量。
|
||||
total_detections:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 服务启动以来的总检测次数。
|
||||
uptime_seconds:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 服务已运行的时间 (秒)。
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/gowvp/gb28181
|
||||
|
||||
go 1.25
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
@@ -15,6 +15,7 @@ require (
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
google.golang.org/grpc v1.78.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.30.3
|
||||
@@ -35,6 +36,7 @@ require (
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -87,11 +89,9 @@ require (
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/protobuf v1.36.10
|
||||
modernc.org/libc v1.61.5 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.34.4 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/gowvp/onvif => ../onvif
|
||||
|
||||
@@ -35,6 +35,10 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -55,6 +59,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -157,6 +163,18 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -191,6 +209,12 @@ golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -33,6 +33,7 @@ func Run(bc *conf.Bootstrap) {
|
||||
defer clean()
|
||||
|
||||
go setupZLM(ctx, bc.ConfigDir)
|
||||
go setupAIClient(ctx, "http://127.0.0.1:15123/ai", bc.Debug)
|
||||
|
||||
// 如果需要执行表迁移,递增此版本号和表更新说明
|
||||
versionapi.DBVersion = "0.0.18"
|
||||
@@ -125,3 +126,64 @@ func setupZLM(ctx context.Context, dir string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findPythonPath() string {
|
||||
candidates := []string{
|
||||
"/opt/homebrew/Caskroom/miniconda/base/bin/python", // macOS Homebrew Miniconda
|
||||
"/opt/homebrew/anaconda3/bin/python", // macOS Homebrew Anaconda
|
||||
"/usr/local/anaconda3/bin/python", // Linux Anaconda
|
||||
"/usr/local/miniconda3/bin/python", // Linux Miniconda
|
||||
"/root/miniconda3/bin/python", // Linux root Miniconda
|
||||
}
|
||||
|
||||
for _, p := range candidates {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return "python"
|
||||
}
|
||||
|
||||
func setupAIClient(ctx context.Context, callback string, debug bool) {
|
||||
workDir := filepath.Join(system.Getwd(), "analysis")
|
||||
if _, err := os.Stat(filepath.Join(workDir, "main.py")); err != nil && os.IsNotExist(err) {
|
||||
slog.Info("main.py 文件不存在,跳过启动 ai", "path", filepath.Join(workDir, "main.py"))
|
||||
return
|
||||
}
|
||||
|
||||
pythonPath := findPythonPath()
|
||||
slog.Info("使用 Python 路径", "path", pythonPath)
|
||||
|
||||
args := []string{"main.py"}
|
||||
if callback != "" {
|
||||
args = append(args, "--callback-url", callback)
|
||||
}
|
||||
if debug {
|
||||
args = append(args, "--log-level", "DEBUG")
|
||||
}
|
||||
|
||||
for range 100 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Info("收到退出信号,停止重启 ai")
|
||||
return
|
||||
default:
|
||||
slog.Info("ai 启动中...")
|
||||
cmd := exec.CommandContext(ctx, pythonPath, args...)
|
||||
cmd.Dir = workDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// 启动命令 - 正常情况下会阻塞在这里
|
||||
if err := cmd.Run(); err != nil {
|
||||
slog.Error("ai 运行失败", "err", err)
|
||||
} else {
|
||||
slog.Info("ai 退出,将重新启动")
|
||||
}
|
||||
|
||||
// 等待后重启(不管是正常退出还是异常退出)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package app
|
||||
|
||||
|
||||
+17
-14
@@ -7,13 +7,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/gowvp/gb28181/internal/conf"
|
||||
"github.com/gowvp/gb28181/internal/data"
|
||||
"github.com/gowvp/gb28181/internal/web/api"
|
||||
"github.com/gowvp/gb28181/pkg/gbs"
|
||||
"github.com/ixugo/goddd/domain/version/versionapi"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Injectors from wire.go:
|
||||
@@ -41,19 +42,21 @@ func wireApp(bc *conf.Bootstrap, log *slog.Logger) (http.Handler, func(), error)
|
||||
proxyAPI := api.NewProxyAPI(proxyCore)
|
||||
configAPI := api.NewConfigAPI(db, bc)
|
||||
userAPI := api.NewUserAPI(bc)
|
||||
aiWebhookAPI := api.NewAIWebhookAPI(bc)
|
||||
usecase := &api.Usecase{
|
||||
Conf: bc,
|
||||
DB: db,
|
||||
Version: versionapiAPI,
|
||||
SMSAPI: smsAPI,
|
||||
WebHookAPI: webHookAPI,
|
||||
UniqueID: uniqueidCore,
|
||||
MediaAPI: pushAPI,
|
||||
GB28181API: ipcapi,
|
||||
ProxyAPI: proxyAPI,
|
||||
ConfigAPI: configAPI,
|
||||
SipServer: server,
|
||||
UserAPI: userAPI,
|
||||
Conf: bc,
|
||||
DB: db,
|
||||
Version: versionapiAPI,
|
||||
SMSAPI: smsAPI,
|
||||
WebHookAPI: webHookAPI,
|
||||
UniqueID: uniqueidCore,
|
||||
MediaAPI: pushAPI,
|
||||
GB28181API: ipcapi,
|
||||
ProxyAPI: proxyAPI,
|
||||
ConfigAPI: configAPI,
|
||||
SipServer: server,
|
||||
UserAPI: userAPI,
|
||||
AIWebhookAPI: aiWebhookAPI,
|
||||
}
|
||||
handler := api.NewHTTPHandler(usecase)
|
||||
return handler, func() {
|
||||
|
||||
@@ -19,8 +19,9 @@ type Server struct {
|
||||
Debug bool
|
||||
RTMPSecret string `comment:"rtmp 推流秘钥"`
|
||||
|
||||
Username string `comment:"登录用户名"`
|
||||
Password string `comment:"登录密码"`
|
||||
Username string `comment:"登录用户名"`
|
||||
Password string `comment:"登录密码"`
|
||||
DisabledAI bool `comment:"是否禁用 ai 分析服务"`
|
||||
|
||||
HTTP ServerHTTP `comment:"对外提供的服务,建议由 nginx 代理"` // HTTP服务器
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package ipc
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -19,7 +20,7 @@ type ChannelStorer interface {
|
||||
Find(context.Context, *[]*Channel, orm.Pager, ...orm.QueryOption) (int64, error)
|
||||
Get(context.Context, *Channel, ...orm.QueryOption) error
|
||||
Add(context.Context, *Channel) error
|
||||
Edit(context.Context, *Channel, func(*Channel), ...orm.QueryOption) error
|
||||
Edit(context.Context, *Channel, func(*Channel) error, ...orm.QueryOption) error
|
||||
Del(context.Context, *Channel, ...orm.QueryOption) error
|
||||
|
||||
BatchEdit(context.Context, string, any, ...orm.QueryOption) error // 批量更新一个字段
|
||||
@@ -87,10 +88,11 @@ func (c *Core) AddChannel(ctx context.Context, in *AddChannelInput) (*Channel, e
|
||||
func (c *Core) EditChannel(ctx context.Context, in *EditChannelInput, id string) (*Channel, error) {
|
||||
// TODO: 修改 onvif 的账号/密码 后需要重新连接设备
|
||||
var out Channel
|
||||
if err := c.store.Channel().Edit(ctx, &out, func(b *Channel) {
|
||||
if err := c.store.Channel().Edit(ctx, &out, func(b *Channel) error {
|
||||
if err := copier.Copy(b, in); err != nil {
|
||||
slog.ErrorContext(ctx, "Copy", "err", err)
|
||||
}
|
||||
return nil
|
||||
}, orm.Where("id=?", id)); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
|
||||
}
|
||||
@@ -105,3 +107,38 @@ func (c *Core) DelChannel(ctx context.Context, id string) (*Channel, error) {
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *Core) AddZone(ctx context.Context, in *AddZoneInput, channelID string) (*Zone, error) {
|
||||
newZone := Zone{
|
||||
Name: in.Name,
|
||||
Coordinates: in.Coordinates,
|
||||
Color: in.Color,
|
||||
Labels: in.Labels,
|
||||
}
|
||||
|
||||
var out Channel
|
||||
if err := c.store.Channel().Edit(ctx, &out, func(b *Channel) error {
|
||||
if slices.ContainsFunc(b.Ext.Zones, func(z Zone) bool {
|
||||
return z.Name == in.Name
|
||||
}) {
|
||||
return reason.ErrBadRequest.SetMsg("存在同名区域")
|
||||
}
|
||||
|
||||
b.Ext.Zones = append(b.Ext.Zones, newZone)
|
||||
return nil
|
||||
}, orm.Where("id=?", channelID)); err != nil {
|
||||
if reason.IsCustomError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
|
||||
}
|
||||
return &newZone, nil
|
||||
}
|
||||
|
||||
func (c *Core) GetZones(ctx context.Context, channelID string) ([]Zone, error) {
|
||||
var out Channel
|
||||
if err := c.store.Channel().Get(ctx, &out, orm.Where("id=?", channelID)); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
|
||||
}
|
||||
return out.Ext.Zones, nil
|
||||
}
|
||||
|
||||
@@ -28,3 +28,11 @@ type AddChannelInput struct {
|
||||
IsOnline bool `json:"is_online"` // 是否在线
|
||||
Ext DeviceExt `json:"ext"`
|
||||
}
|
||||
|
||||
type AddZoneInput struct {
|
||||
Name string `json:"name"` // 区域名称
|
||||
Coordinates []float32 `json:"coordinates"` // 坐标
|
||||
Color string `json:"color"` // 颜色,支持 hex 颜色值,如 #FF0000
|
||||
Labels []string `json:"labels"` // 标签
|
||||
ChannelID string `json:"-"` // 通道 id
|
||||
}
|
||||
|
||||
@@ -83,8 +83,9 @@ func (g Adapter) Edit(deviceID string, changeFn func(*Device)) error {
|
||||
|
||||
func (g Adapter) EditPlayingByID(ctx context.Context, id string, playing bool) error {
|
||||
var ch Channel
|
||||
if err := g.store.Channel().Edit(ctx, &ch, func(c *Channel) {
|
||||
if err := g.store.Channel().Edit(ctx, &ch, func(c *Channel) error {
|
||||
c.IsPlaying = playing
|
||||
return nil
|
||||
}, orm.Where("id=?", id)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,8 +94,9 @@ func (g Adapter) EditPlayingByID(ctx context.Context, id string, playing bool) e
|
||||
|
||||
func (g Adapter) EditPlaying(ctx context.Context, deviceID, channelID string, playing bool) error {
|
||||
var ch Channel
|
||||
if err := g.store.Channel().Edit(ctx, &ch, func(c *Channel) {
|
||||
if err := g.store.Channel().Edit(ctx, &ch, func(c *Channel) error {
|
||||
c.IsPlaying = playing
|
||||
return nil
|
||||
}, orm.Where("device_id = ? AND channel_id = ?", deviceID, channelID)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,10 +147,11 @@ func (g Adapter) SaveChannels(channels []*Channel) error {
|
||||
|
||||
if existing, ok := existingMap[channel.ChannelID]; ok {
|
||||
// 通道已存在,更新信息
|
||||
_ = g.store.Channel().Edit(ctx, existing, func(c *Channel) {
|
||||
_ = g.store.Channel().Edit(ctx, existing, func(c *Channel) error {
|
||||
c.Name = channel.Name
|
||||
c.IsOnline = channel.IsOnline
|
||||
c.Ext = channel.Ext
|
||||
return nil
|
||||
}, orm.Where("id=?", existing.ID))
|
||||
} else {
|
||||
// 通道不存在,新增
|
||||
|
||||
@@ -40,7 +40,7 @@ func (c *Channel) Del(ctx context.Context, ch *ipc.Channel, opts ...orm.QueryOpt
|
||||
}
|
||||
|
||||
// Edit implements ipc.ChannelStorer.
|
||||
func (c *Channel) Edit(ctx context.Context, ch *ipc.Channel, changeFn func(*ipc.Channel), opts ...orm.QueryOption) error {
|
||||
func (c *Channel) Edit(ctx context.Context, ch *ipc.Channel, changeFn func(*ipc.Channel) error, opts ...orm.QueryOption) error {
|
||||
return c.Storer.Channel().Edit(ctx, ch, changeFn, opts...)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ func (d Channel) Add(ctx context.Context, model *ipc.Channel) error {
|
||||
}
|
||||
|
||||
// Edit implements ipc.ChannelStorer.
|
||||
func (d Channel) Edit(ctx context.Context, model *ipc.Channel, changeFn func(*ipc.Channel), opts ...orm.QueryOption) error {
|
||||
return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
|
||||
func (d Channel) Edit(ctx context.Context, model *ipc.Channel, changeFn func(*ipc.Channel) error, opts ...orm.QueryOption) error {
|
||||
return orm.UpdateWithContext2(ctx, d.db, model, changeFn, opts...)
|
||||
}
|
||||
|
||||
// Del implements ipc.ChannelStorer.
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/ixugo/goddd/pkg/web"
|
||||
)
|
||||
|
||||
const KeepaliveInterval = 2 * 15 * time.Second
|
||||
|
||||
type WarpMediaServer struct {
|
||||
IsOnline bool
|
||||
LastUpdatedAt time.Time
|
||||
@@ -71,7 +73,6 @@ func (n *NodeManager) tickCheck() {
|
||||
case <-n.quit:
|
||||
return
|
||||
case <-ticker.C:
|
||||
const KeepaliveInterval = 2 * 15 * time.Second
|
||||
n.cacheServers.Range(func(_ string, ms *WarpMediaServer) bool {
|
||||
if time.Since(ms.LastUpdatedAt) < KeepaliveInterval {
|
||||
ms.IsOnline = true
|
||||
@@ -234,6 +235,14 @@ func (n *NodeManager) Keepalive(serverID string) {
|
||||
value.LastUpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func (n *NodeManager) IsOnline(serverID string) bool {
|
||||
value, ok := n.cacheServers.Load(serverID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return value.IsOnline
|
||||
}
|
||||
|
||||
// findMediaServer Paginated search
|
||||
func (n *NodeManager) findMediaServer(ctx context.Context, in *FindMediaServerInput) ([]*MediaServer, int64, error) {
|
||||
items := make([]*MediaServer, 0)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/gowvp/gb28181/protos"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var _ protos.AnalysisServiceClient = (*AIClient)(nil)
|
||||
|
||||
// AIClient 封装 gRPC 检测服务客户端,提供统一的 AI 检测调用入口
|
||||
type AIClient struct {
|
||||
cli protos.AnalysisServiceClient
|
||||
}
|
||||
|
||||
// NewAIClient 创建 AI 检测客户端实例
|
||||
func NewAIClient(addr string) *AIClient {
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
slog.Error("NewAiClient", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
p := protos.NewHealthClient(conn)
|
||||
resp, err := p.Check(context.Background(), &protos.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
slog.Error("HealthCheck", "err", err)
|
||||
return
|
||||
}
|
||||
if resp.GetStatus() == protos.HealthCheckResponse_SERVING {
|
||||
slog.Info("HealthCheck OK", "resp", resp)
|
||||
} else {
|
||||
slog.Error("HealthCheck", "resp", resp)
|
||||
}
|
||||
}()
|
||||
|
||||
cli := protos.NewAnalysisServiceClient(conn)
|
||||
return &AIClient{cli: cli}
|
||||
}
|
||||
|
||||
// GetStatus implements [protos.AnalysisServiceClient].
|
||||
func (a *AIClient) GetStatus(ctx context.Context, in *protos.StatusRequest, opts ...grpc.CallOption) (*protos.StatusResponse, error) {
|
||||
return a.cli.GetStatus(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// StartCamera implements [protos.AnalysisServiceClient].
|
||||
func (a *AIClient) StartCamera(ctx context.Context, in *protos.StartCameraRequest, opts ...grpc.CallOption) (*protos.StartCameraResponse, error) {
|
||||
if in.GetDetectFps() == 0 {
|
||||
in.DetectFps = 5
|
||||
}
|
||||
if in.GetThreshold() == 0 {
|
||||
in.Threshold = 0.5
|
||||
}
|
||||
if in.GetRetryLimit() == 0 {
|
||||
in.RetryLimit = 10
|
||||
}
|
||||
return a.cli.StartCamera(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// StopCamera implements [protos.AnalysisServiceClient].
|
||||
func (a *AIClient) StopCamera(ctx context.Context, in *protos.StopCameraRequest, opts ...grpc.CallOption) (*protos.StopCameraResponse, error) {
|
||||
return a.cli.StopCamera(ctx, in, opts...)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/gowvp/gb28181/protos"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
addr := "localhost:50051"
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cli := protos.NewHealthClient(conn)
|
||||
resp, err := cli.Check(context.Background(), &protos.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slog.Info("HealthCheck", "resp", resp)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gowvp/gb28181/internal/conf"
|
||||
"github.com/gowvp/gb28181/internal/rpc"
|
||||
"github.com/ixugo/goddd/pkg/conc"
|
||||
"github.com/ixugo/goddd/pkg/system"
|
||||
"github.com/ixugo/goddd/pkg/web"
|
||||
)
|
||||
|
||||
// AIWebhookAPI 处理 AI 分析服务的回调请求
|
||||
type AIWebhookAPI struct {
|
||||
log *slog.Logger
|
||||
conf *conf.Bootstrap
|
||||
aiTasks *conc.Map[string, struct{}]
|
||||
ai *rpc.AIClient
|
||||
}
|
||||
|
||||
// NewAIWebhookAPI 创建 AI Webhook API 实例
|
||||
func NewAIWebhookAPI(conf *conf.Bootstrap) AIWebhookAPI {
|
||||
return AIWebhookAPI{
|
||||
log: slog.With("hook", "ai"),
|
||||
conf: conf,
|
||||
ai: rpc.NewAIClient("127.0.0.1:50051"),
|
||||
aiTasks: conc.NewMap[string, struct{}](),
|
||||
}
|
||||
}
|
||||
|
||||
// registerAIWebhookAPI 注册 AI 回调路由,接收来自 Python AI 服务的各类事件通知
|
||||
func registerAIWebhookAPI(r gin.IRouter, api AIWebhookAPI, handler ...gin.HandlerFunc) {
|
||||
group := r.Group("/ai", handler...)
|
||||
group.POST("/keepalive", web.WrapH(api.onKeepalive))
|
||||
group.POST("/started", web.WrapH(api.onStarted))
|
||||
group.POST("/events", web.WrapH(api.onEvents))
|
||||
group.POST("/stopped", web.WrapH(api.onStopped))
|
||||
}
|
||||
|
||||
// onKeepalive 接收 AI 服务心跳,用于监控 AI 服务存活状态
|
||||
func (a AIWebhookAPI) onKeepalive(c *gin.Context, in *AIKeepaliveInput) (AIWebhookOutput, error) {
|
||||
var activeStreams int
|
||||
var uptimeSeconds int64
|
||||
if in.Stats != nil {
|
||||
activeStreams = in.Stats.ActiveStreams
|
||||
uptimeSeconds = in.Stats.UptimeSeconds
|
||||
}
|
||||
a.log.InfoContext(c.Request.Context(), "ai keepalive",
|
||||
"timestamp", in.Timestamp,
|
||||
"message", in.Message,
|
||||
"active_streams", activeStreams,
|
||||
"uptime_seconds", uptimeSeconds,
|
||||
)
|
||||
return newAIWebhookOutputOK(), nil
|
||||
}
|
||||
|
||||
// onStarted 接收 AI 服务启动通知,确认 AI 服务已就绪
|
||||
func (a AIWebhookAPI) onStarted(c *gin.Context, in *AIStartedInput) (AIWebhookOutput, error) {
|
||||
a.log.InfoContext(c.Request.Context(), "ai started",
|
||||
"timestamp", in.Timestamp,
|
||||
"message", in.Message,
|
||||
)
|
||||
return newAIWebhookOutputOK(), nil
|
||||
}
|
||||
|
||||
// onEvents 接收 AI 检测事件,将快照保存到临时目录
|
||||
func (a AIWebhookAPI) onEvents(c *gin.Context, in *AIDetectionInput) (AIWebhookOutput, error) {
|
||||
a.log.InfoContext(c.Request.Context(), "ai detection event",
|
||||
"camera_id", in.CameraID,
|
||||
"timestamp", in.Timestamp,
|
||||
"detection_count", len(in.Detections),
|
||||
"snapshot_size", fmt.Sprintf("%dx%d", in.SnapshotWidth, in.SnapshotHeight),
|
||||
)
|
||||
|
||||
for i, det := range in.Detections {
|
||||
a.log.InfoContext(c.Request.Context(), "detection detail",
|
||||
"index", i,
|
||||
"label", det.Label,
|
||||
"confidence", det.Confidence,
|
||||
"box", fmt.Sprintf("(%d,%d)-(%d,%d)", det.Box.XMin, det.Box.YMin, det.Box.XMax, det.Box.YMax),
|
||||
"area", det.Area,
|
||||
)
|
||||
}
|
||||
|
||||
if in.Snapshot != "" {
|
||||
if err := saveSnapshot(in.CameraID, in.Timestamp, in.Snapshot); err != nil {
|
||||
a.log.ErrorContext(c.Request.Context(), "save snapshot failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return newAIWebhookOutputOK(), nil
|
||||
}
|
||||
|
||||
// onStopped 接收 AI 任务停止通知,记录停止原因
|
||||
func (a AIWebhookAPI) onStopped(c *gin.Context, in *AIStoppedInput) (AIWebhookOutput, error) {
|
||||
a.log.InfoContext(c.Request.Context(), "ai task stopped",
|
||||
"camera_id", in.CameraID,
|
||||
"timestamp", in.Timestamp,
|
||||
"reason", in.Reason,
|
||||
"message", in.Message,
|
||||
)
|
||||
a.aiTasks.Delete(in.CameraID)
|
||||
return newAIWebhookOutputOK(), nil
|
||||
}
|
||||
|
||||
// saveSnapshot 将 Base64 编码的快照保存到临时目录
|
||||
func saveSnapshot(cameraID string, timestamp int64, snapshotB64 string) error {
|
||||
tmpDir := filepath.Join(system.Getwd(), "configs", "demo")
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(snapshotB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode base64: %w", err)
|
||||
}
|
||||
|
||||
t := time.UnixMilli(timestamp)
|
||||
filename := fmt.Sprintf("%s_%s.jpg", cameraID, t.Format("20060102_150405"))
|
||||
filePath := filepath.Join(tmpDir, filename)
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
|
||||
return fmt.Errorf("create tmp dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("write file: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("snapshot saved", "path", filePath, "size", len(data))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
// AIKeepaliveInput 心跳回调请求体
|
||||
type AIKeepaliveInput struct {
|
||||
Timestamp int64 `json:"timestamp"` // Unix 时间戳 (毫秒)
|
||||
Stats *AIGlobalStats `json:"stats"` // 全局统计信息
|
||||
Message string `json:"message"` // 附加消息
|
||||
}
|
||||
|
||||
// AIStartedInput 服务启动回调请求体
|
||||
type AIStartedInput struct {
|
||||
Timestamp int64 `json:"timestamp"` // Unix 时间戳 (毫秒)
|
||||
Message string `json:"message"` // 启动消息
|
||||
}
|
||||
|
||||
// AIDetectionInput 检测事件回调请求体
|
||||
type AIDetectionInput struct {
|
||||
CameraID string `json:"camera_id"` // 摄像头 ID
|
||||
Timestamp int64 `json:"timestamp"` // Unix 时间戳 (毫秒)
|
||||
Detections []AIDetection `json:"detections"` // 检测结果列表
|
||||
Snapshot string `json:"snapshot"` // Base64 编码的快照 (JPEG)
|
||||
SnapshotWidth int `json:"snapshot_width"` // 快照宽度
|
||||
SnapshotHeight int `json:"snapshot_height"` // 快照高度
|
||||
}
|
||||
|
||||
// AIStoppedInput 任务停止回调请求体
|
||||
type AIStoppedInput struct {
|
||||
CameraID string `json:"camera_id"` // 摄像头 ID
|
||||
Timestamp int64 `json:"timestamp"` // Unix 时间戳 (毫秒)
|
||||
Reason string `json:"reason"` // 停止原因 (user_requested, error)
|
||||
Message string `json:"message"` // 详细信息
|
||||
}
|
||||
|
||||
// AIDetection 检测对象
|
||||
type AIDetection struct {
|
||||
Label string `json:"label"` // 物体类别
|
||||
Confidence float64 `json:"confidence"` // 置信度 (0.0 - 1.0)
|
||||
Box AIBoundingBox `json:"box"` // 像素坐标边界框
|
||||
Area int `json:"area"` // 边界框像素面积
|
||||
NormBox *AINormBox `json:"norm_box"` // 归一化边界框
|
||||
}
|
||||
|
||||
// AIBoundingBox 像素坐标边界框
|
||||
type AIBoundingBox struct {
|
||||
XMin int `json:"x_min"`
|
||||
YMin int `json:"y_min"`
|
||||
XMax int `json:"x_max"`
|
||||
YMax int `json:"y_max"`
|
||||
}
|
||||
|
||||
// AINormBox 归一化边界框
|
||||
type AINormBox struct {
|
||||
X float64 `json:"x"` // 中心点 X 坐标
|
||||
Y float64 `json:"y"` // 中心点 Y 坐标
|
||||
W float64 `json:"w"` // 宽度
|
||||
H float64 `json:"h"` // 高度
|
||||
}
|
||||
|
||||
// AIGlobalStats 全局统计信息
|
||||
type AIGlobalStats struct {
|
||||
ActiveStreams int `json:"active_streams"` // 活跃流数量
|
||||
TotalDetections int64 `json:"total_detections"` // 总检测次数
|
||||
UptimeSeconds int64 `json:"uptime_seconds"` // 运行时间 (秒)
|
||||
}
|
||||
|
||||
// AIWebhookOutput 通用响应体
|
||||
type AIWebhookOutput struct {
|
||||
Code int `json:"code"` // 错误代码,0 表示成功
|
||||
Msg string `json:"msg"` // 消息
|
||||
}
|
||||
|
||||
func newAIWebhookOutputOK() AIWebhookOutput {
|
||||
return AIWebhookOutput{Code: 0, Msg: "success"}
|
||||
}
|
||||
@@ -105,6 +105,9 @@ func setupRouter(r *gin.Engine, uc *Usecase) {
|
||||
|
||||
// 反向代理流媒体数据
|
||||
r.Any("/proxy/sms/*path", uc.proxySMS)
|
||||
|
||||
// 注册 AI 分析服务回调接口
|
||||
registerAIWebhookAPI(r, uc.AIWebhookAPI)
|
||||
}
|
||||
|
||||
type playOutput struct {
|
||||
|
||||
+33
-1
@@ -2,6 +2,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/gowvp/gb28181/internal/core/push"
|
||||
"github.com/gowvp/gb28181/internal/core/sms"
|
||||
"github.com/gowvp/gb28181/pkg/zlm"
|
||||
"github.com/gowvp/gb28181/protos"
|
||||
"github.com/ixugo/goddd/domain/uniqueid"
|
||||
"github.com/ixugo/goddd/pkg/hook"
|
||||
"github.com/ixugo/goddd/pkg/orm"
|
||||
@@ -252,6 +254,9 @@ func (a IPCAPI) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
|
||||
return nil, reason.ErrNotFound.SetMsg("不支持的播放通道")
|
||||
}
|
||||
|
||||
if !a.uc.SMSAPI.smsCore.IsOnline(mediaServerID) {
|
||||
return nil, reason.ErrNotFound.SetMsg("Oops! 流媒体服务离线或IP有误")
|
||||
}
|
||||
svr, err := a.uc.SMSAPI.smsCore.GetMediaServer(c.Request.Context(), mediaServerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -284,9 +289,9 @@ func (a IPCAPI) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
|
||||
|
||||
// 取一张快照
|
||||
go func() {
|
||||
rtsp := fmt.Sprintf("rtsp://%s:%d/%s", "127.0.0.1", svr.Ports.RTSP, stream) + "?" + session
|
||||
for range 2 {
|
||||
time.Sleep(3 * time.Second)
|
||||
rtsp := fmt.Sprintf("rtsp://%s:%d/%s", "127.0.0.1", svr.Ports.RTSP, stream) + "?" + session
|
||||
|
||||
body, err := a.uc.SMSAPI.smsCore.GetSnapshot(svr, sms.GetSnapRequest{
|
||||
GetSnapRequest: zlm.GetSnapRequest{
|
||||
@@ -305,6 +310,33 @@ func (a IPCAPI) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
|
||||
}
|
||||
break
|
||||
}
|
||||
if a.uc.Conf.Server.DisabledAI || a.uc.AIWebhookAPI.ai == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := a.uc.AIWebhookAPI.aiTasks.LoadOrStore(channelID, struct{}{}); !ok {
|
||||
resp, err := a.uc.AIWebhookAPI.ai.StartCamera(context.Background(), &protos.StartCameraRequest{
|
||||
CameraId: appStream,
|
||||
CameraName: appStream,
|
||||
RtspUrl: rtsp,
|
||||
DetectFps: 5,
|
||||
Labels: []string{"person", "car", "cat", "dog"},
|
||||
Threshold: 0.65,
|
||||
RetryLimit: 10,
|
||||
CallbackUrl: fmt.Sprintf("http://127.0.0.1:%d/ai", a.uc.Conf.Server.HTTP.Port),
|
||||
CallbackSecret: "Basic 1234567890",
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("start camera", "err", err)
|
||||
return
|
||||
}
|
||||
slog.Debug("start camera", "resp", resp,
|
||||
"msg", resp.GetMessage(),
|
||||
"source_width", resp.GetSourceWidth(),
|
||||
"source_height", resp.GetSourceHeight(),
|
||||
"source_fps", resp.GetSourceFps(),
|
||||
)
|
||||
}
|
||||
}()
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ var (
|
||||
NewProxyAPI, NewProxyCore,
|
||||
NewConfigAPI,
|
||||
NewUserAPI,
|
||||
NewAIWebhookAPI,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -55,25 +56,23 @@ type Usecase struct {
|
||||
ProxyAPI ProxyAPI
|
||||
ConfigAPI ConfigAPI
|
||||
|
||||
SipServer *gbs.Server
|
||||
UserAPI UserAPI
|
||||
SipServer *gbs.Server
|
||||
UserAPI UserAPI
|
||||
AIWebhookAPI AIWebhookAPI
|
||||
}
|
||||
|
||||
// NewHTTPHandler 生成Gin框架路由内容
|
||||
func NewHTTPHandler(uc *Usecase) http.Handler {
|
||||
cfg := uc.Conf.Server
|
||||
// 检查是否设置了 JWT 密钥,如果未设置,则生成一个长度为 32 的随机字符串作为密钥
|
||||
if cfg.HTTP.JwtSecret == "" {
|
||||
uc.Conf.Server.HTTP.JwtSecret = orm.GenerateRandomString(32) // 生成一个长度为 32 的随机字符串作为密钥
|
||||
uc.Conf.Server.HTTP.JwtSecret = orm.GenerateRandomString(32)
|
||||
}
|
||||
// 如果不处于调试模式,将 Gin 设置为发布模式
|
||||
if !cfg.Debug {
|
||||
gin.SetMode(gin.ReleaseMode) // 将 Gin 设置为发布模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
g := gin.New() // 创建一个新的 Gin 实例
|
||||
// 处理未找到路由的情况,返回 JSON 格式的 404 错误信息
|
||||
g := gin.New()
|
||||
g.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(404, "来到了无人的荒漠") // 返回 JSON 格式的 404 错误信息
|
||||
c.JSON(404, "来到了无人的荒漠")
|
||||
})
|
||||
// 如果启用了 Pprof,设置 Pprof 监控
|
||||
if cfg.HTTP.PProf.Enabled {
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
package ffwork
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ixugo/goddd/pkg/queue"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Width, Height int
|
||||
FPS int
|
||||
RTSPURL string
|
||||
Transport string
|
||||
UseWallClock bool
|
||||
HWAccel string
|
||||
OnFrame func(frame *FrameData)
|
||||
Name string
|
||||
}
|
||||
FrameData struct {
|
||||
FrameNum uint64
|
||||
Timestamp time.Time
|
||||
Data []byte
|
||||
}
|
||||
FrameCapture struct {
|
||||
Name string
|
||||
config Config
|
||||
frameSize int
|
||||
FrameCh chan *FrameData
|
||||
errCh chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
m sync.Mutex
|
||||
started bool
|
||||
cmd *exec.Cmd
|
||||
lastFrame time.Time
|
||||
wg sync.WaitGroup
|
||||
ffmpegLog *queue.CirQueue[string]
|
||||
frameCount, skipCount uint64
|
||||
OnFrame func(frame *FrameData)
|
||||
}
|
||||
Stats struct {
|
||||
Name string
|
||||
FrameCount, SkipCount uint64
|
||||
LastFrame time.Time
|
||||
FrameSize int
|
||||
IsRunning bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewFrameCapture(cfg Config) (*FrameCapture, error) {
|
||||
if cfg.Width <= 0 || cfg.Height <= 0 {
|
||||
return nil, fmt.Errorf("invalid resolution: %dx%d", cfg.Width, cfg.Height)
|
||||
}
|
||||
if cfg.FPS <= 0 {
|
||||
return nil, fmt.Errorf("invalid fps: %d", cfg.FPS)
|
||||
}
|
||||
if cfg.RTSPURL == "" {
|
||||
return nil, fmt.Errorf("resp url is required")
|
||||
}
|
||||
if cfg.Transport == "" {
|
||||
cfg.Transport = "tcp"
|
||||
}
|
||||
frameSize := cfg.Width * cfg.Height * 3 / 2
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &FrameCapture{
|
||||
config: cfg,
|
||||
frameSize: frameSize,
|
||||
FrameCh: make(chan *FrameData, 10),
|
||||
errCh: make(chan error, 1),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
ffmpegLog: queue.NewCirQueue[string](100),
|
||||
OnFrame: cfg.OnFrame,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) FrameSize() int {
|
||||
return fc.frameSize
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) buildFFmpegArgs() []string {
|
||||
args := []string{
|
||||
"-hide_banner",
|
||||
"-loglevel", "warning",
|
||||
"-threads", "2",
|
||||
}
|
||||
args = append(args, "-user_agent", "FFmpeg GoWVP")
|
||||
args = append(args, "-avoid_negative_ts", "make_zero",
|
||||
"-fflags", "+genpts+discardcorrupt",
|
||||
"-rtsp_transport", fc.config.Transport,
|
||||
"-timeout", "10000000",
|
||||
)
|
||||
if fc.config.UseWallClock {
|
||||
args = append(args, "-use_wallclock_as_timestamps", "1")
|
||||
}
|
||||
if fc.config.HWAccel != "" {
|
||||
args = append(args, "-hwaccel", fc.config.HWAccel)
|
||||
}
|
||||
args = append(args, "-i", fc.config.RTSPURL)
|
||||
|
||||
args = append(args,
|
||||
"-f", "rawvideo",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-r", strconv.Itoa(fc.config.FPS),
|
||||
"-vf", fmt.Sprintf("fps=%d,scale=%d:%d", fc.config.FPS, fc.config.Width, fc.config.Height),
|
||||
"pipe:1",
|
||||
)
|
||||
return args
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) Start() error {
|
||||
fc.m.Lock()
|
||||
defer fc.m.Unlock()
|
||||
if fc.started {
|
||||
return fmt.Errorf("frame capture already started")
|
||||
}
|
||||
|
||||
args := fc.buildFFmpegArgs()
|
||||
fc.cmd = exec.CommandContext(fc.ctx, "ffmpeg", args...)
|
||||
stdout, err := fc.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := fc.cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stderr pipe: %w", err)
|
||||
}
|
||||
if err := fc.cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start ffmpeg: %w", err)
|
||||
}
|
||||
fc.started = true
|
||||
fc.lastFrame = time.Now()
|
||||
|
||||
fc.wg.Go(func() { fc.captureLoop(stdout) })
|
||||
fc.wg.Go(func() { fc.readStderr(stderr) })
|
||||
return nil
|
||||
}
|
||||
|
||||
// captureLoop 从 ffmpeg 的 stdout 读取原始视频帧数据
|
||||
// ffmpeg 输出的是固定大小的 YUV420P 格式帧,需要按帧大小读取
|
||||
func (fc *FrameCapture) captureLoop(stdout io.Reader) {
|
||||
defer close(fc.FrameCh)
|
||||
|
||||
reader := bufio.NewReaderSize(stdout, fc.frameSize*10)
|
||||
for {
|
||||
select {
|
||||
case <-fc.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
frameBytes := make([]byte, fc.frameSize)
|
||||
n, err := io.ReadFull(reader, frameBytes)
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
select {
|
||||
case fc.errCh <- fmt.Errorf("ffmpeg stream ended: %w", err):
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case fc.errCh <- fmt.Errorf("failed to read frame: %w", err):
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
if n != fc.frameSize {
|
||||
select {
|
||||
case fc.errCh <- fmt.Errorf("incomplete frame: %d != %d", n, fc.frameSize):
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
frameNum := atomic.AddUint64(&fc.frameCount, 1)
|
||||
now := time.Now()
|
||||
fc.m.Lock()
|
||||
fc.lastFrame = now
|
||||
fc.m.Unlock()
|
||||
|
||||
frame := FrameData{
|
||||
FrameNum: frameNum,
|
||||
Timestamp: now,
|
||||
Data: frameBytes,
|
||||
}
|
||||
if fc.OnFrame != nil {
|
||||
fc.OnFrame(&frame)
|
||||
}
|
||||
|
||||
select {
|
||||
case fc.FrameCh <- &frame:
|
||||
case <-fc.ctx.Done():
|
||||
return
|
||||
default:
|
||||
atomic.AddUint64(&fc.skipCount, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readStderr 读取 ffmpeg 的 stderr 输出用于日志记录
|
||||
// ffmpeg 的警告和错误信息都会输出到 stderr
|
||||
func (fc *FrameCapture) readStderr(stderr io.Reader) {
|
||||
scan := bufio.NewScanner(stderr)
|
||||
for scan.Scan() {
|
||||
line := scan.Text()
|
||||
fc.ffmpegLog.Push(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) Frames() <-chan *FrameData {
|
||||
return fc.FrameCh
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) Error() <-chan error {
|
||||
return fc.errCh
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) Log() []string {
|
||||
return fc.ffmpegLog.Range()
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) GetFrame(timeout time.Duration) (*FrameData, error) {
|
||||
select {
|
||||
case frame, ok := <-fc.FrameCh:
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("frame channel closed")
|
||||
}
|
||||
return frame, nil
|
||||
case err := <-fc.errCh:
|
||||
return nil, err
|
||||
case <-fc.ctx.Done():
|
||||
return nil, fc.ctx.Err()
|
||||
case <-time.After(timeout):
|
||||
return nil, fmt.Errorf("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) Stop() error {
|
||||
fc.m.Lock()
|
||||
if !fc.started {
|
||||
fc.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
fc.m.Unlock()
|
||||
if cancel := fc.cancel; cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
fc.wg.Wait()
|
||||
|
||||
if fc.cmd != nil && fc.cmd.Process != nil {
|
||||
done := make(chan error, 1)
|
||||
defer close(done)
|
||||
go func() {
|
||||
done <- fc.cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
if err := fc.cmd.Process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill ffmpeg: %w", err)
|
||||
}
|
||||
<-done
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fc *FrameCapture) GetStats() Stats {
|
||||
fc.m.Lock()
|
||||
defer fc.m.Unlock()
|
||||
return Stats{
|
||||
Name: fc.config.Name,
|
||||
FrameCount: atomic.LoadUint64(&fc.frameCount),
|
||||
SkipCount: atomic.LoadUint64(&fc.skipCount),
|
||||
LastFrame: fc.lastFrame,
|
||||
FrameSize: fc.frameSize,
|
||||
IsRunning: fc.started,
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestGetSnapshot(t *testing.T) {
|
||||
if len(imageData) >= 8 {
|
||||
pngHeader := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
|
||||
isPNG := true
|
||||
for i := 0; i < 8; i++ {
|
||||
for i := range 8 {
|
||||
if imageData[i] != pngHeader[i] {
|
||||
isPNG = false
|
||||
break
|
||||
|
||||
@@ -184,14 +184,6 @@ type GetServerConfigData struct {
|
||||
SrtTimeoutSec string `json:"srt.timeoutSec"`
|
||||
}
|
||||
|
||||
func NewString(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func NewBool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// SetServerConfigRequest
|
||||
// https://github.com/zlmediakit/ZLMediaKit/wiki/MediaServer%E6%94%AF%E6%8C%81%E7%9A%84HTTP-HOOK-API
|
||||
type SetServerConfigRequest struct {
|
||||
|
||||
@@ -65,7 +65,7 @@ func (c *CircleQueue) Range() []PercentData {
|
||||
idx = c.idx
|
||||
}
|
||||
data := make([]PercentData, 0, size)
|
||||
for i := 0; i < size; i++ {
|
||||
for range size {
|
||||
data = append(data, c.array[idx])
|
||||
idx = (idx + 1) % c.maxSize
|
||||
}
|
||||
|
||||
@@ -0,0 +1,818 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v6.33.0
|
||||
// source: protos/analysis.proto
|
||||
|
||||
package protos
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type HealthCheckResponse_ServingStatus int32
|
||||
|
||||
const (
|
||||
HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0
|
||||
HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 // 服务正常
|
||||
HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 // 服务不可用 (加载模型中)
|
||||
)
|
||||
|
||||
// Enum value maps for HealthCheckResponse_ServingStatus.
|
||||
var (
|
||||
HealthCheckResponse_ServingStatus_name = map[int32]string{
|
||||
0: "UNKNOWN",
|
||||
1: "SERVING",
|
||||
2: "NOT_SERVING",
|
||||
}
|
||||
HealthCheckResponse_ServingStatus_value = map[string]int32{
|
||||
"UNKNOWN": 0,
|
||||
"SERVING": 1,
|
||||
"NOT_SERVING": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus {
|
||||
p := new(HealthCheckResponse_ServingStatus)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x HealthCheckResponse_ServingStatus) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_protos_analysis_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {
|
||||
return &file_protos_analysis_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead.
|
||||
func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{9, 0}
|
||||
}
|
||||
|
||||
// 启动摄像头分析请求
|
||||
type StartCameraRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// === 基础信息 ===
|
||||
CameraId string `protobuf:"bytes,1,opt,name=camera_id,json=cameraId,proto3" json:"camera_id,omitempty"` // 摄像头唯一标识 (必填)
|
||||
CameraName string `protobuf:"bytes,2,opt,name=camera_name,json=cameraName,proto3" json:"camera_name,omitempty"` // 摄像头名称,用于日志 (可选)
|
||||
RtspUrl string `protobuf:"bytes,3,opt,name=rtsp_url,json=rtspUrl,proto3" json:"rtsp_url,omitempty"` // RTSP 子码流地址 (必填)
|
||||
// === 检测配置 ===
|
||||
DetectFps int32 `protobuf:"varint,4,opt,name=detect_fps,json=detectFps,proto3" json:"detect_fps,omitempty"` // 检测帧率,默认 5
|
||||
Labels []string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty"` // 要检测的标签,如 ["person", "car"],空则检测全部
|
||||
Threshold float32 `protobuf:"fixed32,6,opt,name=threshold,proto3" json:"threshold,omitempty"` // 置信度阈值,默认 0.5
|
||||
// === ROI 区域 (多边形点位) ===
|
||||
// n 个坐标组合成的多边形,例如 [0.1, 0.2, 0.12, 0.22, 0.1, 0.3...]
|
||||
// 归一化坐标 (x1, y1, x2, y2, ...)
|
||||
RoiPoints []float32 `protobuf:"fixed32,7,rep,packed,name=roi_points,json=roiPoints,proto3" json:"roi_points,omitempty"`
|
||||
// 错误处理
|
||||
RetryLimit int32 `protobuf:"varint,8,opt,name=retry_limit,json=retryLimit,proto3" json:"retry_limit,omitempty"` // 遇到错误的自动重试次数,默认 10
|
||||
// === 回调配置 ===
|
||||
// 优先级高于服务启动时的默认配置
|
||||
CallbackUrl string `protobuf:"bytes,10,opt,name=callback_url,json=callbackUrl,proto3" json:"callback_url,omitempty"` // HTTP 回调地址 (必填)
|
||||
CallbackSecret string `protobuf:"bytes,11,opt,name=callback_secret,json=callbackSecret,proto3" json:"callback_secret,omitempty"` // 回调签名密钥 (可选,用于验证)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) Reset() {
|
||||
*x = StartCameraRequest{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StartCameraRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StartCameraRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StartCameraRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StartCameraRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetCameraId() string {
|
||||
if x != nil {
|
||||
return x.CameraId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetCameraName() string {
|
||||
if x != nil {
|
||||
return x.CameraName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetRtspUrl() string {
|
||||
if x != nil {
|
||||
return x.RtspUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetDetectFps() int32 {
|
||||
if x != nil {
|
||||
return x.DetectFps
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetLabels() []string {
|
||||
if x != nil {
|
||||
return x.Labels
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetThreshold() float32 {
|
||||
if x != nil {
|
||||
return x.Threshold
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetRoiPoints() []float32 {
|
||||
if x != nil {
|
||||
return x.RoiPoints
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetRetryLimit() int32 {
|
||||
if x != nil {
|
||||
return x.RetryLimit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetCallbackUrl() string {
|
||||
if x != nil {
|
||||
return x.CallbackUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StartCameraRequest) GetCallbackSecret() string {
|
||||
if x != nil {
|
||||
return x.CallbackSecret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StartCameraResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
// 流信息 (通过 ffprobe 探测返回)
|
||||
SourceWidth int32 `protobuf:"varint,3,opt,name=source_width,json=sourceWidth,proto3" json:"source_width,omitempty"` // 原始分辨率宽度
|
||||
SourceHeight int32 `protobuf:"varint,4,opt,name=source_height,json=sourceHeight,proto3" json:"source_height,omitempty"` // 原始分辨率高度
|
||||
SourceFps float32 `protobuf:"fixed32,5,opt,name=source_fps,json=sourceFps,proto3" json:"source_fps,omitempty"` // 原始帧率
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) Reset() {
|
||||
*x = StartCameraResponse{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StartCameraResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StartCameraResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StartCameraResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StartCameraResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) GetSourceWidth() int32 {
|
||||
if x != nil {
|
||||
return x.SourceWidth
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) GetSourceHeight() int32 {
|
||||
if x != nil {
|
||||
return x.SourceHeight
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StartCameraResponse) GetSourceFps() float32 {
|
||||
if x != nil {
|
||||
return x.SourceFps
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type StopCameraRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
CameraId string `protobuf:"bytes,1,opt,name=camera_id,json=cameraId,proto3" json:"camera_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StopCameraRequest) Reset() {
|
||||
*x = StopCameraRequest{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StopCameraRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StopCameraRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StopCameraRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StopCameraRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StopCameraRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *StopCameraRequest) GetCameraId() string {
|
||||
if x != nil {
|
||||
return x.CameraId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StopCameraResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StopCameraResponse) Reset() {
|
||||
*x = StopCameraResponse{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StopCameraResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StopCameraResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StopCameraResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StopCameraResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StopCameraResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *StopCameraResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StopCameraResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StatusRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StatusRequest) Reset() {
|
||||
*x = StatusRequest{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StatusRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StatusRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
IsReady bool `protobuf:"varint,1,opt,name=is_ready,json=isReady,proto3" json:"is_ready,omitempty"` // AI 模型是否加载完成
|
||||
Cameras []*CameraStatus `protobuf:"bytes,2,rep,name=cameras,proto3" json:"cameras,omitempty"` // 摄像头状态列表
|
||||
Stats *GlobalStats `protobuf:"bytes,3,opt,name=stats,proto3" json:"stats,omitempty"` // 全局统计信息
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StatusResponse) Reset() {
|
||||
*x = StatusResponse{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StatusResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetIsReady() bool {
|
||||
if x != nil {
|
||||
return x.IsReady
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetCameras() []*CameraStatus {
|
||||
if x != nil {
|
||||
return x.Cameras
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetStats() *GlobalStats {
|
||||
if x != nil {
|
||||
return x.Stats
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CameraStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
CameraId string `protobuf:"bytes,1,opt,name=camera_id,json=cameraId,proto3" json:"camera_id,omitempty"`
|
||||
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` // "running", "error", "stopped"
|
||||
FramesProcessed int64 `protobuf:"varint,3,opt,name=frames_processed,json=framesProcessed,proto3" json:"frames_processed,omitempty"` // 已处理帧数
|
||||
LastError string `protobuf:"bytes,4,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` // 最后一次错误信息
|
||||
RetryCount int32 `protobuf:"varint,5,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"` // 当前重试次数
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CameraStatus) Reset() {
|
||||
*x = CameraStatus{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CameraStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CameraStatus) ProtoMessage() {}
|
||||
|
||||
func (x *CameraStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CameraStatus.ProtoReflect.Descriptor instead.
|
||||
func (*CameraStatus) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *CameraStatus) GetCameraId() string {
|
||||
if x != nil {
|
||||
return x.CameraId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CameraStatus) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CameraStatus) GetFramesProcessed() int64 {
|
||||
if x != nil {
|
||||
return x.FramesProcessed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CameraStatus) GetLastError() string {
|
||||
if x != nil {
|
||||
return x.LastError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CameraStatus) GetRetryCount() int32 {
|
||||
if x != nil {
|
||||
return x.RetryCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GlobalStats struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ActiveStreams int32 `protobuf:"varint,1,opt,name=active_streams,json=activeStreams,proto3" json:"active_streams,omitempty"` // 当前活跃 RTSP 流数量
|
||||
TotalDetections int64 `protobuf:"varint,2,opt,name=total_detections,json=totalDetections,proto3" json:"total_detections,omitempty"` // 服务启动以来的总检测次数
|
||||
UptimeSeconds int64 `protobuf:"varint,3,opt,name=uptime_seconds,json=uptimeSeconds,proto3" json:"uptime_seconds,omitempty"` // 运行时间
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GlobalStats) Reset() {
|
||||
*x = GlobalStats{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GlobalStats) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GlobalStats) ProtoMessage() {}
|
||||
|
||||
func (x *GlobalStats) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GlobalStats.ProtoReflect.Descriptor instead.
|
||||
func (*GlobalStats) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *GlobalStats) GetActiveStreams() int32 {
|
||||
if x != nil {
|
||||
return x.ActiveStreams
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GlobalStats) GetTotalDetections() int64 {
|
||||
if x != nil {
|
||||
return x.TotalDetections
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GlobalStats) GetUptimeSeconds() int64 {
|
||||
if x != nil {
|
||||
return x.UptimeSeconds
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type HealthCheckRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthCheckRequest) Reset() {
|
||||
*x = HealthCheckRequest{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthCheckRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthCheckRequest) ProtoMessage() {}
|
||||
|
||||
func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.
|
||||
func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
type HealthCheckResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=analysis.HealthCheckResponse_ServingStatus" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthCheckResponse) Reset() {
|
||||
*x = HealthCheckResponse{}
|
||||
mi := &file_protos_analysis_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthCheckResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthCheckResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protos_analysis_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HealthCheckResponse) Descriptor() ([]byte, []int) {
|
||||
return file_protos_analysis_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return HealthCheckResponse_UNKNOWN
|
||||
}
|
||||
|
||||
var File_protos_analysis_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_protos_analysis_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x15protos/analysis.proto\x12\banalysis\"\xce\x02\n" +
|
||||
"\x12StartCameraRequest\x12\x1b\n" +
|
||||
"\tcamera_id\x18\x01 \x01(\tR\bcameraId\x12\x1f\n" +
|
||||
"\vcamera_name\x18\x02 \x01(\tR\n" +
|
||||
"cameraName\x12\x19\n" +
|
||||
"\brtsp_url\x18\x03 \x01(\tR\artspUrl\x12\x1d\n" +
|
||||
"\n" +
|
||||
"detect_fps\x18\x04 \x01(\x05R\tdetectFps\x12\x16\n" +
|
||||
"\x06labels\x18\x05 \x03(\tR\x06labels\x12\x1c\n" +
|
||||
"\tthreshold\x18\x06 \x01(\x02R\tthreshold\x12\x1d\n" +
|
||||
"\n" +
|
||||
"roi_points\x18\a \x03(\x02R\troiPoints\x12\x1f\n" +
|
||||
"\vretry_limit\x18\b \x01(\x05R\n" +
|
||||
"retryLimit\x12!\n" +
|
||||
"\fcallback_url\x18\n" +
|
||||
" \x01(\tR\vcallbackUrl\x12'\n" +
|
||||
"\x0fcallback_secret\x18\v \x01(\tR\x0ecallbackSecret\"\xb0\x01\n" +
|
||||
"\x13StartCameraResponse\x12\x18\n" +
|
||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12!\n" +
|
||||
"\fsource_width\x18\x03 \x01(\x05R\vsourceWidth\x12#\n" +
|
||||
"\rsource_height\x18\x04 \x01(\x05R\fsourceHeight\x12\x1d\n" +
|
||||
"\n" +
|
||||
"source_fps\x18\x05 \x01(\x02R\tsourceFps\"0\n" +
|
||||
"\x11StopCameraRequest\x12\x1b\n" +
|
||||
"\tcamera_id\x18\x01 \x01(\tR\bcameraId\"H\n" +
|
||||
"\x12StopCameraResponse\x12\x18\n" +
|
||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\"\x0f\n" +
|
||||
"\rStatusRequest\"\x8a\x01\n" +
|
||||
"\x0eStatusResponse\x12\x19\n" +
|
||||
"\bis_ready\x18\x01 \x01(\bR\aisReady\x120\n" +
|
||||
"\acameras\x18\x02 \x03(\v2\x16.analysis.CameraStatusR\acameras\x12+\n" +
|
||||
"\x05stats\x18\x03 \x01(\v2\x15.analysis.GlobalStatsR\x05stats\"\xae\x01\n" +
|
||||
"\fCameraStatus\x12\x1b\n" +
|
||||
"\tcamera_id\x18\x01 \x01(\tR\bcameraId\x12\x16\n" +
|
||||
"\x06status\x18\x02 \x01(\tR\x06status\x12)\n" +
|
||||
"\x10frames_processed\x18\x03 \x01(\x03R\x0fframesProcessed\x12\x1d\n" +
|
||||
"\n" +
|
||||
"last_error\x18\x04 \x01(\tR\tlastError\x12\x1f\n" +
|
||||
"\vretry_count\x18\x05 \x01(\x05R\n" +
|
||||
"retryCount\"\x86\x01\n" +
|
||||
"\vGlobalStats\x12%\n" +
|
||||
"\x0eactive_streams\x18\x01 \x01(\x05R\ractiveStreams\x12)\n" +
|
||||
"\x10total_detections\x18\x02 \x01(\x03R\x0ftotalDetections\x12%\n" +
|
||||
"\x0euptime_seconds\x18\x03 \x01(\x03R\ruptimeSeconds\"\x14\n" +
|
||||
"\x12HealthCheckRequest\"\x96\x01\n" +
|
||||
"\x13HealthCheckResponse\x12C\n" +
|
||||
"\x06status\x18\x01 \x01(\x0e2+.analysis.HealthCheckResponse.ServingStatusR\x06status\":\n" +
|
||||
"\rServingStatus\x12\v\n" +
|
||||
"\aUNKNOWN\x10\x00\x12\v\n" +
|
||||
"\aSERVING\x10\x01\x12\x0f\n" +
|
||||
"\vNOT_SERVING\x10\x022\xe6\x01\n" +
|
||||
"\x0fAnalysisService\x12J\n" +
|
||||
"\vStartCamera\x12\x1c.analysis.StartCameraRequest\x1a\x1d.analysis.StartCameraResponse\x12G\n" +
|
||||
"\n" +
|
||||
"StopCamera\x12\x1b.analysis.StopCameraRequest\x1a\x1c.analysis.StopCameraResponse\x12>\n" +
|
||||
"\tGetStatus\x12\x17.analysis.StatusRequest\x1a\x18.analysis.StatusResponse2N\n" +
|
||||
"\x06Health\x12D\n" +
|
||||
"\x05Check\x12\x1c.analysis.HealthCheckRequest\x1a\x1d.analysis.HealthCheckResponseB\n" +
|
||||
"Z\b./protosb\x06proto3"
|
||||
|
||||
var (
|
||||
file_protos_analysis_proto_rawDescOnce sync.Once
|
||||
file_protos_analysis_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_protos_analysis_proto_rawDescGZIP() []byte {
|
||||
file_protos_analysis_proto_rawDescOnce.Do(func() {
|
||||
file_protos_analysis_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protos_analysis_proto_rawDesc), len(file_protos_analysis_proto_rawDesc)))
|
||||
})
|
||||
return file_protos_analysis_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_protos_analysis_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_protos_analysis_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
||||
var file_protos_analysis_proto_goTypes = []any{
|
||||
(HealthCheckResponse_ServingStatus)(0), // 0: analysis.HealthCheckResponse.ServingStatus
|
||||
(*StartCameraRequest)(nil), // 1: analysis.StartCameraRequest
|
||||
(*StartCameraResponse)(nil), // 2: analysis.StartCameraResponse
|
||||
(*StopCameraRequest)(nil), // 3: analysis.StopCameraRequest
|
||||
(*StopCameraResponse)(nil), // 4: analysis.StopCameraResponse
|
||||
(*StatusRequest)(nil), // 5: analysis.StatusRequest
|
||||
(*StatusResponse)(nil), // 6: analysis.StatusResponse
|
||||
(*CameraStatus)(nil), // 7: analysis.CameraStatus
|
||||
(*GlobalStats)(nil), // 8: analysis.GlobalStats
|
||||
(*HealthCheckRequest)(nil), // 9: analysis.HealthCheckRequest
|
||||
(*HealthCheckResponse)(nil), // 10: analysis.HealthCheckResponse
|
||||
}
|
||||
var file_protos_analysis_proto_depIdxs = []int32{
|
||||
7, // 0: analysis.StatusResponse.cameras:type_name -> analysis.CameraStatus
|
||||
8, // 1: analysis.StatusResponse.stats:type_name -> analysis.GlobalStats
|
||||
0, // 2: analysis.HealthCheckResponse.status:type_name -> analysis.HealthCheckResponse.ServingStatus
|
||||
1, // 3: analysis.AnalysisService.StartCamera:input_type -> analysis.StartCameraRequest
|
||||
3, // 4: analysis.AnalysisService.StopCamera:input_type -> analysis.StopCameraRequest
|
||||
5, // 5: analysis.AnalysisService.GetStatus:input_type -> analysis.StatusRequest
|
||||
9, // 6: analysis.Health.Check:input_type -> analysis.HealthCheckRequest
|
||||
2, // 7: analysis.AnalysisService.StartCamera:output_type -> analysis.StartCameraResponse
|
||||
4, // 8: analysis.AnalysisService.StopCamera:output_type -> analysis.StopCameraResponse
|
||||
6, // 9: analysis.AnalysisService.GetStatus:output_type -> analysis.StatusResponse
|
||||
10, // 10: analysis.Health.Check:output_type -> analysis.HealthCheckResponse
|
||||
7, // [7:11] is the sub-list for method output_type
|
||||
3, // [3:7] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_protos_analysis_proto_init() }
|
||||
func file_protos_analysis_proto_init() {
|
||||
if File_protos_analysis_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protos_analysis_proto_rawDesc), len(file_protos_analysis_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 10,
|
||||
NumExtensions: 0,
|
||||
NumServices: 2,
|
||||
},
|
||||
GoTypes: file_protos_analysis_proto_goTypes,
|
||||
DependencyIndexes: file_protos_analysis_proto_depIdxs,
|
||||
EnumInfos: file_protos_analysis_proto_enumTypes,
|
||||
MessageInfos: file_protos_analysis_proto_msgTypes,
|
||||
}.Build()
|
||||
File_protos_analysis_proto = out.File
|
||||
file_protos_analysis_proto_goTypes = nil
|
||||
file_protos_analysis_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package analysis;
|
||||
|
||||
option go_package = "./protos";
|
||||
|
||||
service AnalysisService {
|
||||
// 启动摄像头分析
|
||||
rpc StartCamera(StartCameraRequest) returns (StartCameraResponse);
|
||||
|
||||
// 停止摄像头分析
|
||||
rpc StopCamera(StopCameraRequest) returns (StopCameraResponse);
|
||||
|
||||
// 获取服务状态
|
||||
rpc GetStatus(StatusRequest) returns (StatusResponse);
|
||||
}
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
|
||||
// 启动摄像头分析请求
|
||||
message StartCameraRequest {
|
||||
// === 基础信息 ===
|
||||
string camera_id = 1; // 摄像头唯一标识 (必填)
|
||||
string camera_name = 2; // 摄像头名称,用于日志 (可选)
|
||||
string rtsp_url = 3; // RTSP 子码流地址 (必填)
|
||||
|
||||
// === 检测配置 ===
|
||||
int32 detect_fps = 4; // 检测帧率,默认 5
|
||||
repeated string labels = 5; // 要检测的标签,如 ["person", "car"],空则检测全部
|
||||
float threshold = 6; // 置信度阈值,默认 0.5
|
||||
|
||||
// === ROI 区域 (多边形点位) ===
|
||||
// n 个坐标组合成的多边形,例如 [0.1, 0.2, 0.12, 0.22, 0.1, 0.3...]
|
||||
// 归一化坐标 (x1, y1, x2, y2, ...)
|
||||
repeated float roi_points = 7;
|
||||
|
||||
// 错误处理
|
||||
int32 retry_limit = 8; // 遇到错误的自动重试次数,默认 10
|
||||
|
||||
// === 回调配置 ===
|
||||
// 优先级高于服务启动时的默认配置
|
||||
string callback_url = 10; // HTTP 回调地址 (必填)
|
||||
string callback_secret = 11; // 回调签名密钥 (可选,用于验证)
|
||||
}
|
||||
|
||||
message StartCameraResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
|
||||
// 流信息 (通过 ffprobe 探测返回)
|
||||
int32 source_width = 3; // 原始分辨率宽度
|
||||
int32 source_height = 4; // 原始分辨率高度
|
||||
float source_fps = 5; // 原始帧率
|
||||
}
|
||||
|
||||
message StopCameraRequest {
|
||||
string camera_id = 1;
|
||||
}
|
||||
|
||||
message StopCameraResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message StatusRequest {}
|
||||
|
||||
message StatusResponse {
|
||||
bool is_ready = 1; // AI 模型是否加载完成
|
||||
repeated CameraStatus cameras = 2; // 摄像头状态列表
|
||||
GlobalStats stats = 3; // 全局统计信息
|
||||
}
|
||||
|
||||
message CameraStatus {
|
||||
string camera_id = 1;
|
||||
string status = 2; // "running", "error", "stopped"
|
||||
int64 frames_processed = 3; // 已处理帧数
|
||||
string last_error = 4; // 最后一次错误信息
|
||||
int32 retry_count = 5; // 当前重试次数
|
||||
}
|
||||
|
||||
message GlobalStats {
|
||||
int32 active_streams = 1; // 当前活跃 RTSP 流数量
|
||||
int64 total_detections = 2; // 服务启动以来的总检测次数
|
||||
int64 uptime_seconds = 3; // 运行时间
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 健康检查相关
|
||||
// =============================================================================
|
||||
|
||||
message HealthCheckRequest {}
|
||||
|
||||
message HealthCheckResponse {
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0;
|
||||
SERVING = 1; // 服务正常
|
||||
NOT_SERVING = 2;// 服务不可用 (加载模型中)
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: protos/analysis.proto
|
||||
|
||||
package protos
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AnalysisService_StartCamera_FullMethodName = "/analysis.AnalysisService/StartCamera"
|
||||
AnalysisService_StopCamera_FullMethodName = "/analysis.AnalysisService/StopCamera"
|
||||
AnalysisService_GetStatus_FullMethodName = "/analysis.AnalysisService/GetStatus"
|
||||
)
|
||||
|
||||
// AnalysisServiceClient is the client API for AnalysisService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AnalysisServiceClient interface {
|
||||
// 启动摄像头分析
|
||||
StartCamera(ctx context.Context, in *StartCameraRequest, opts ...grpc.CallOption) (*StartCameraResponse, error)
|
||||
// 停止摄像头分析
|
||||
StopCamera(ctx context.Context, in *StopCameraRequest, opts ...grpc.CallOption) (*StopCameraResponse, error)
|
||||
// 获取服务状态
|
||||
GetStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
|
||||
}
|
||||
|
||||
type analysisServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAnalysisServiceClient(cc grpc.ClientConnInterface) AnalysisServiceClient {
|
||||
return &analysisServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *analysisServiceClient) StartCamera(ctx context.Context, in *StartCameraRequest, opts ...grpc.CallOption) (*StartCameraResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StartCameraResponse)
|
||||
err := c.cc.Invoke(ctx, AnalysisService_StartCamera_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *analysisServiceClient) StopCamera(ctx context.Context, in *StopCameraRequest, opts ...grpc.CallOption) (*StopCameraResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StopCameraResponse)
|
||||
err := c.cc.Invoke(ctx, AnalysisService_StopCamera_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *analysisServiceClient) GetStatus(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(StatusResponse)
|
||||
err := c.cc.Invoke(ctx, AnalysisService_GetStatus_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AnalysisServiceServer is the server API for AnalysisService service.
|
||||
// All implementations must embed UnimplementedAnalysisServiceServer
|
||||
// for forward compatibility.
|
||||
type AnalysisServiceServer interface {
|
||||
// 启动摄像头分析
|
||||
StartCamera(context.Context, *StartCameraRequest) (*StartCameraResponse, error)
|
||||
// 停止摄像头分析
|
||||
StopCamera(context.Context, *StopCameraRequest) (*StopCameraResponse, error)
|
||||
// 获取服务状态
|
||||
GetStatus(context.Context, *StatusRequest) (*StatusResponse, error)
|
||||
mustEmbedUnimplementedAnalysisServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAnalysisServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAnalysisServiceServer struct{}
|
||||
|
||||
func (UnimplementedAnalysisServiceServer) StartCamera(context.Context, *StartCameraRequest) (*StartCameraResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method StartCamera not implemented")
|
||||
}
|
||||
func (UnimplementedAnalysisServiceServer) StopCamera(context.Context, *StopCameraRequest) (*StopCameraResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method StopCamera not implemented")
|
||||
}
|
||||
func (UnimplementedAnalysisServiceServer) GetStatus(context.Context, *StatusRequest) (*StatusResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented")
|
||||
}
|
||||
func (UnimplementedAnalysisServiceServer) mustEmbedUnimplementedAnalysisServiceServer() {}
|
||||
func (UnimplementedAnalysisServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAnalysisServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AnalysisServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAnalysisServiceServer interface {
|
||||
mustEmbedUnimplementedAnalysisServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAnalysisServiceServer(s grpc.ServiceRegistrar, srv AnalysisServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedAnalysisServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AnalysisService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AnalysisService_StartCamera_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StartCameraRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AnalysisServiceServer).StartCamera(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AnalysisService_StartCamera_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AnalysisServiceServer).StartCamera(ctx, req.(*StartCameraRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AnalysisService_StopCamera_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StopCameraRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AnalysisServiceServer).StopCamera(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AnalysisService_StopCamera_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AnalysisServiceServer).StopCamera(ctx, req.(*StopCameraRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AnalysisService_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AnalysisServiceServer).GetStatus(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AnalysisService_GetStatus_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AnalysisServiceServer).GetStatus(ctx, req.(*StatusRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AnalysisService_ServiceDesc is the grpc.ServiceDesc for AnalysisService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AnalysisService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "analysis.AnalysisService",
|
||||
HandlerType: (*AnalysisServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "StartCamera",
|
||||
Handler: _AnalysisService_StartCamera_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "StopCamera",
|
||||
Handler: _AnalysisService_StopCamera_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetStatus",
|
||||
Handler: _AnalysisService_GetStatus_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "protos/analysis.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
Health_Check_FullMethodName = "/analysis.Health/Check"
|
||||
)
|
||||
|
||||
// HealthClient is the client API for Health service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type HealthClient interface {
|
||||
Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
|
||||
}
|
||||
|
||||
type healthClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewHealthClient(cc grpc.ClientConnInterface) HealthClient {
|
||||
return &healthClient{cc}
|
||||
}
|
||||
|
||||
func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HealthCheckResponse)
|
||||
err := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HealthServer is the server API for Health service.
|
||||
// All implementations must embed UnimplementedHealthServer
|
||||
// for forward compatibility.
|
||||
type HealthServer interface {
|
||||
Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
|
||||
mustEmbedUnimplementedHealthServer()
|
||||
}
|
||||
|
||||
// UnimplementedHealthServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedHealthServer struct{}
|
||||
|
||||
func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Check not implemented")
|
||||
}
|
||||
func (UnimplementedHealthServer) mustEmbedUnimplementedHealthServer() {}
|
||||
func (UnimplementedHealthServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeHealthServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to HealthServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeHealthServer interface {
|
||||
mustEmbedUnimplementedHealthServer()
|
||||
}
|
||||
|
||||
func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) {
|
||||
// If the following call panics, it indicates UnimplementedHealthServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Health_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HealthCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HealthServer).Check(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Health_Check_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Health_ServiceDesc is the grpc.ServiceDesc for Health service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Health_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "analysis.Health",
|
||||
HandlerType: (*HealthServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Check",
|
||||
Handler: _Health_Check_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "protos/analysis.proto",
|
||||
}
|
||||
Reference in New Issue
Block a user