修改部署脚本,media-server和agent独立部署
This commit is contained in:
parent
c343b2bc91
commit
317a8b1097
@ -94,6 +94,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 自动启动 media-server(如果启用)
|
||||||
|
if cfg.Agent.MediaServerProcess.Enable {
|
||||||
|
time.Sleep(500 * time.Millisecond) // 等待 HTTP 服务启动
|
||||||
|
log.Info("auto-starting media-server...")
|
||||||
|
if err := h.StartMediaServer(""); err != nil {
|
||||||
|
log.Error("auto-start media-server failed: " + err.Error())
|
||||||
|
} else {
|
||||||
|
log.Info("media-server auto-started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Agent.DiscoveryEnable {
|
if cfg.Agent.DiscoveryEnable {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ type Server struct {
|
|||||||
agentCfg config.AgentConfig
|
agentCfg config.AgentConfig
|
||||||
ms *mediaserver.Client
|
ms *mediaserver.Client
|
||||||
store *modelstore.Store
|
store *modelstore.Store
|
||||||
proc *procctl.Controller
|
proc procctl.ProcessController
|
||||||
audit *audit.Recorder
|
audit *audit.Recorder
|
||||||
tasks *tasks.Registry
|
tasks *tasks.Registry
|
||||||
baseDir string
|
baseDir string
|
||||||
@ -46,6 +46,12 @@ type Server struct {
|
|||||||
cpuMu sync.Mutex
|
cpuMu sync.Mutex
|
||||||
lastCPU metrics.CPUStat
|
lastCPU metrics.CPUStat
|
||||||
lastCPUTS time.Time
|
lastCPUTS time.Time
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP 实现 http.Handler 接口
|
||||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoResponse struct {
|
type InfoResponse struct {
|
||||||
@ -62,10 +68,14 @@ type InfoResponse struct {
|
|||||||
UptimeSec int64 `json:"uptime_sec"`
|
UptimeSec int64 `json:"uptime_sec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, store *modelstore.Store, deviceID string, agentPort int, mediaPort int, version, gitSHA string) http.Handler {
|
func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, store *modelstore.Store, deviceID string, agentPort int, mediaPort int, version, gitSHA string) *Server {
|
||||||
var pc *procctl.Controller
|
var pc procctl.ProcessController
|
||||||
if agentCfg.MediaServerProcess.Enable {
|
if agentCfg.MediaServerProcess.Enable {
|
||||||
|
// 使用传统的 fork/exec 模式
|
||||||
pc = procctl.New(agentCfg, baseDir)
|
pc = procctl.New(agentCfg, baseDir)
|
||||||
|
} else {
|
||||||
|
// 使用 systemctl 模式管理 Media Server
|
||||||
|
pc = procctl.NewSystemCtlController("media-server")
|
||||||
}
|
}
|
||||||
execPath, _ := os.Executable()
|
execPath, _ := os.Executable()
|
||||||
if execPath != "" {
|
if execPath != "" {
|
||||||
@ -119,7 +129,8 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
|
|||||||
mux.HandleFunc("/v1/assets", s.handleAssets)
|
mux.HandleFunc("/v1/assets", s.handleAssets)
|
||||||
mux.HandleFunc("/v1/tasks/", s.handleTask)
|
mux.HandleFunc("/v1/tasks/", s.handleTask)
|
||||||
|
|
||||||
return mux
|
s.handler = mux
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -436,6 +447,15 @@ type mediaProcReq struct {
|
|||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartMediaServer 启动 media-server(供外部调用)
|
||||||
|
func (s *Server) StartMediaServer(configName string) error {
|
||||||
|
if s.proc == nil || !s.proc.Enabled() {
|
||||||
|
return errors.New("media server process management not enabled")
|
||||||
|
}
|
||||||
|
_, err := s.proc.Start(configName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleMediaStart(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleMediaStart(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
|||||||
@ -23,6 +23,19 @@ var ErrConflict = errors.New("conflict")
|
|||||||
var ErrInvalidConfig = errors.New("invalid config")
|
var ErrInvalidConfig = errors.New("invalid config")
|
||||||
var ErrConfigNotFound = errors.New("config not found")
|
var ErrConfigNotFound = errors.New("config not found")
|
||||||
|
|
||||||
|
// ProcessController 定义进程控制器接口
|
||||||
|
type ProcessController interface {
|
||||||
|
Enabled() bool
|
||||||
|
Status() (Status, error)
|
||||||
|
Version() (string, error)
|
||||||
|
Start(configName string) (Status, error)
|
||||||
|
Stop() (Status, error)
|
||||||
|
Restart(configName string) (Status, error)
|
||||||
|
BinaryInfo() (BinaryUpdateResult, error)
|
||||||
|
UpdateBinary(r io.Reader, contentLength int64, expectedSha256 string) (BinaryUpdateResult, error)
|
||||||
|
RollbackBinary(backupPath string) (BinaryUpdateResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
Pid int `json:"pid"`
|
Pid int `json:"pid"`
|
||||||
|
|||||||
109
agent/internal/procctl/systemctl.go
Normal file
109
agent/internal/procctl/systemctl.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package procctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemCtlController 使用 systemctl 管理 Media Server
|
||||||
|
type SystemCtlController struct {
|
||||||
|
serviceName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSystemCtlController 创建 systemctl 控制器
|
||||||
|
func NewSystemCtlController(serviceName string) *SystemCtlController {
|
||||||
|
return &SystemCtlController{
|
||||||
|
serviceName: serviceName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Enabled() bool { return s != nil }
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Status() (Status, error) {
|
||||||
|
// 检查服务是否运行
|
||||||
|
cmd := exec.Command("systemctl", "is-active", "--quiet", s.serviceName)
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// 服务未运行
|
||||||
|
return Status{Running: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 PID
|
||||||
|
pidCmd := exec.Command("systemctl", "show", "--property=MainPID", "--value", s.serviceName)
|
||||||
|
out, err := pidCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return Status{Running: true}, nil // 运行但无法获取 PID
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, _ := strconv.Atoi(strings.TrimSpace(string(out)))
|
||||||
|
if pid <= 0 {
|
||||||
|
return Status{Running: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status{
|
||||||
|
Running: true,
|
||||||
|
Pid: pid,
|
||||||
|
ConfigPath: "/opt/rk3588-media-server/etc/media-server.json",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Version() (string, error) {
|
||||||
|
// 直接执行 media-server --version
|
||||||
|
cmd := exec.Command("/opt/rk3588-media-server/bin/media-server", "--version")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get version failed: %w", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Start(configName string) (Status, error) {
|
||||||
|
// 使用 systemctl start
|
||||||
|
cmd := exec.Command("systemctl", "start", s.serviceName)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return Status{}, fmt.Errorf("start failed: %w, stderr: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待服务启动
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
return s.Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Stop() (Status, error) {
|
||||||
|
cmd := exec.Command("systemctl", "stop", s.serviceName)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return Status{}, fmt.Errorf("stop failed: %w", err)
|
||||||
|
}
|
||||||
|
return Status{Running: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) Restart(configName string) (Status, error) {
|
||||||
|
cmd := exec.Command("systemctl", "restart", s.serviceName)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return Status{}, fmt.Errorf("restart failed: %w", err)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
return s.Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二进制更新相关功能在 systemctl 模式下不支持
|
||||||
|
func (s *SystemCtlController) BinaryInfo() (BinaryUpdateResult, error) {
|
||||||
|
return BinaryUpdateResult{}, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) UpdateBinary(r io.Reader, contentLength int64, expectedSha256 string) (BinaryUpdateResult, error) {
|
||||||
|
return BinaryUpdateResult{}, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemCtlController) RollbackBinary(backupPath string) (BinaryUpdateResult, error) {
|
||||||
|
return BinaryUpdateResult{}, ErrNotSupported
|
||||||
|
}
|
||||||
BIN
agent/rk3588-agent_linux_arm64
Normal file → Executable file
BIN
agent/rk3588-agent_linux_arm64
Normal file → Executable file
Binary file not shown.
@ -159,14 +159,14 @@
|
|||||||
"template": "security_pipeline",
|
"template": "security_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam1",
|
"name": "cam1",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "minioadmin",
|
"minio_ak": "minioadmin",
|
||||||
"minio_sk": "minioadmin"
|
"minio_sk": "minioadmin"
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
@ -194,7 +194,7 @@
|
|||||||
"quality": 85,
|
"quality": 85,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "vi",
|
"bucket": "vi",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "admin",
|
"access_key": "admin",
|
||||||
@ -209,7 +209,7 @@
|
|||||||
"fps": 30,
|
"fps": 30,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "vi",
|
"bucket": "vi",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "admin",
|
"access_key": "admin",
|
||||||
@ -257,7 +257,7 @@
|
|||||||
"quality": 85,
|
"quality": 85,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "vi",
|
"bucket": "vi",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "admin",
|
"access_key": "admin",
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
@ -130,7 +130,7 @@
|
|||||||
"quality": 85,
|
"quality": 85,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "test",
|
"bucket": "test",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "your-access-key",
|
"access_key": "your-access-key",
|
||||||
@ -145,7 +145,7 @@
|
|||||||
"fps": 30,
|
"fps": 30,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "test",
|
"bucket": "test",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "your-access-key",
|
"access_key": "your-access-key",
|
||||||
@ -180,7 +180,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
|
|||||||
@ -171,14 +171,14 @@
|
|||||||
"template": "strict_minio_alarm_pipeline",
|
"template": "strict_minio_alarm_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam1",
|
"name": "cam1",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "your-access-key",
|
"minio_ak": "your-access-key",
|
||||||
"minio_sk": "your-secret-key"
|
"minio_sk": "your-secret-key"
|
||||||
@ -189,14 +189,14 @@
|
|||||||
"template": "strict_minio_alarm_pipeline",
|
"template": "strict_minio_alarm_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam2",
|
"name": "cam2",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "your-access-key",
|
"minio_ak": "your-access-key",
|
||||||
"minio_sk": "your-secret-key"
|
"minio_sk": "your-secret-key"
|
||||||
@ -207,14 +207,14 @@
|
|||||||
"template": "strict_minio_alarm_pipeline",
|
"template": "strict_minio_alarm_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam3",
|
"name": "cam3",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "your-access-key",
|
"minio_ak": "your-access-key",
|
||||||
"minio_sk": "your-secret-key"
|
"minio_sk": "your-secret-key"
|
||||||
@ -225,14 +225,14 @@
|
|||||||
"template": "strict_minio_alarm_pipeline",
|
"template": "strict_minio_alarm_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam4",
|
"name": "cam4",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "your-access-key",
|
"minio_ak": "your-access-key",
|
||||||
"minio_sk": "your-secret-key"
|
"minio_sk": "your-secret-key"
|
||||||
@ -243,14 +243,14 @@
|
|||||||
"template": "strict_minio_alarm_pipeline",
|
"template": "strict_minio_alarm_pipeline",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "cam5",
|
"name": "cam5",
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"src_w": 1280,
|
"src_w": 1280,
|
||||||
"src_h": 720,
|
"src_h": 720,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"gop": 60,
|
"gop": 60,
|
||||||
"bitrate_kbps": 2000,
|
"bitrate_kbps": 2000,
|
||||||
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
"model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn",
|
||||||
"minio_endpoint": "http://10.0.0.5:9000",
|
"minio_endpoint": "http://10.0.0.49:9000",
|
||||||
"minio_bucket": "test",
|
"minio_bucket": "test",
|
||||||
"minio_ak": "your-access-key",
|
"minio_ak": "your-access-key",
|
||||||
"minio_sk": "your-secret-key"
|
"minio_sk": "your-secret-key"
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"type": "input_rtsp",
|
"type": "input_rtsp",
|
||||||
"role": "source",
|
"role": "source",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"url": "rtsp://10.0.0.5:8554/cam",
|
"url": "rtsp://10.0.0.49:8554/cam",
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 720,
|
"height": 720,
|
||||||
@ -92,7 +92,7 @@
|
|||||||
"quality": 85,
|
"quality": 85,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "test",
|
"bucket": "test",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "minioadmin",
|
"access_key": "minioadmin",
|
||||||
@ -107,7 +107,7 @@
|
|||||||
"fps": 25,
|
"fps": 25,
|
||||||
"upload": {
|
"upload": {
|
||||||
"type": "minio",
|
"type": "minio",
|
||||||
"endpoint": "http://10.0.0.5:9000",
|
"endpoint": "http://10.0.0.49:9000",
|
||||||
"bucket": "test",
|
"bucket": "test",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"access_key": "minioadmin",
|
"access_key": "minioadmin",
|
||||||
|
|||||||
@ -19,3 +19,6 @@ ffplay rtsp://localhost:8555/live/cam1_face_det
|
|||||||
|
|
||||||
- 在 Windows 上用 VLC 播放处理后的流:
|
- 在 Windows 上用 VLC 播放处理后的流:
|
||||||
rtsp://3588的IP:8555/live/cam1_face_det
|
rtsp://3588的IP:8555/live/cam1_face_det
|
||||||
|
|
||||||
|
- 编译agent
|
||||||
|
go build -o rk3588-agent_linux_arm64 ./cmd/rk3588-agent
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# RK3588 Media Server + Agent 统一管理脚本
|
# RK3588 Media Server + Agent 统一管理脚本
|
||||||
|
# Media Server 和 Agent 是独立的 systemd 服务
|
||||||
# 用法: sudo ./manage.sh [install|status|uninstall]
|
# 用法: sudo ./manage.sh [install|status|uninstall]
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@ -25,7 +26,7 @@ cmd_install() {
|
|||||||
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
echo -e "${RED}错误: 请使用 sudo 运行此脚本${NC}"
|
echo -e "${RED}错误: 请使用 sudo 运行此脚本${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@ -48,24 +49,23 @@ cmd_install() {
|
|||||||
# 检查插件
|
# 检查插件
|
||||||
if [ ! -d "$BUILD_DIR/plugins" ] || [ -z "$(ls -A $BUILD_DIR/plugins/*.so 2>/dev/null)" ]; then
|
if [ ! -d "$BUILD_DIR/plugins" ] || [ -z "$(ls -A $BUILD_DIR/plugins/*.so 2>/dev/null)" ]; then
|
||||||
echo -e "${YELLOW}警告: 未找到插件目录或插件为空${NC}"
|
echo -e "${YELLOW}警告: 未找到插件目录或插件为空${NC}"
|
||||||
read -p "是否继续? (y/N): " -n 1 -r
|
read -p "是否继续? (y/N): " -r
|
||||||
echo
|
[ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ] && exit 1
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 Agent 编译产物(强制要求)
|
# 检查 Agent 编译产物(强制要求)
|
||||||
AGENT_SOURCE=""
|
AGENT_SOURCE=""
|
||||||
if [ -f "$PROJECT_DIR/agent/rk3588-agent_linux_arm64" ]; then
|
if [ -f "$PROJECT_DIR/agent/rk3588-agent_linux_arm64" ]; then
|
||||||
AGENT_SOURCE="$PROJECT_DIR/agent/rk3588-agent_linux_arm64"
|
AGENT_SOURCE="$PROJECT_DIR/agent/rk3588-agent_linux_arm64"
|
||||||
elif [ -f "$PROJECT_DIR/agent/cmd/rk3588-agent" ]; then
|
elif [ -f "$PROJECT_DIR/agent/cmd/rk3588-agent/rk3588-agent" ]; then
|
||||||
AGENT_SOURCE="$PROJECT_DIR/agent/cmd/rk3588-agent"
|
AGENT_SOURCE="$PROJECT_DIR/agent/cmd/rk3588-agent/rk3588-agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$AGENT_SOURCE" ]; then
|
if [ -z "$AGENT_SOURCE" ]; then
|
||||||
echo -e "${RED}错误: 未找到编译好的 rk3588-agent${NC}"
|
echo -e "${RED}错误: 未找到编译好的 rk3588-agent${NC}"
|
||||||
echo "请先编译 Agent:"
|
echo "请先编译 Agent:"
|
||||||
echo " cd $PROJECT_DIR/agent"
|
echo " cd $PROJECT_DIR/agent"
|
||||||
echo " GOOS=linux GOARCH=arm64 go build -o rk3588-agent_linux_arm64 ./cmd"
|
echo " go build -o rk3588-agent_linux_arm64 ./cmd/rk3588-agent"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -79,7 +79,8 @@ cmd_install() {
|
|||||||
echo " 3) test_cam1_face_det_recog_rtsp_server.json (人脸识别)"
|
echo " 3) test_cam1_face_det_recog_rtsp_server.json (人脸识别)"
|
||||||
echo " 4) sample_cam1.json (完整功能,需手动修复)"
|
echo " 4) sample_cam1.json (完整功能,需手动修复)"
|
||||||
echo ""
|
echo ""
|
||||||
read -p "请输入选项 [1-4,默认1]: " choice
|
printf "请输入选项 [1-4,默认1]: "
|
||||||
|
read choice
|
||||||
choice=${choice:-1}
|
choice=${choice:-1}
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
@ -95,7 +96,12 @@ cmd_install() {
|
|||||||
# 安装 Media Server
|
# 安装 Media Server
|
||||||
echo -e "${YELLOW}[1/3] 安装 Media Server...${NC}"
|
echo -e "${YELLOW}[1/3] 安装 Media Server...${NC}"
|
||||||
rm -rf "$INSTALL_DIR"
|
rm -rf "$INSTALL_DIR"
|
||||||
mkdir -p "$INSTALL_DIR"/{bin/plugins,lib,etc,logs,models,web/hls}
|
mkdir -p "$INSTALL_DIR/bin/plugins"
|
||||||
|
mkdir -p "$INSTALL_DIR/lib"
|
||||||
|
mkdir -p "$INSTALL_DIR/etc"
|
||||||
|
mkdir -p "$INSTALL_DIR/logs"
|
||||||
|
mkdir -p "$INSTALL_DIR/models"
|
||||||
|
mkdir -p "$INSTALL_DIR/web/hls"
|
||||||
|
|
||||||
cp "$BUILD_DIR/media-server" "$INSTALL_DIR/bin/"
|
cp "$BUILD_DIR/media-server" "$INSTALL_DIR/bin/"
|
||||||
chmod +x "$INSTALL_DIR/bin/media-server"
|
chmod +x "$INSTALL_DIR/bin/media-server"
|
||||||
@ -127,6 +133,8 @@ project_dir = os.path.expanduser("~/apps/OrangePi3588Media")
|
|||||||
def fix_path(path):
|
def fix_path(path):
|
||||||
if not path or not isinstance(path, str):
|
if not path or not isinstance(path, str):
|
||||||
return path
|
return path
|
||||||
|
if 'third_party/rknpu2' in path:
|
||||||
|
return os.path.join(install_dir, 'models', os.path.basename(path))
|
||||||
if path.startswith('./'):
|
if path.startswith('./'):
|
||||||
if path.startswith('./web/'):
|
if path.startswith('./web/'):
|
||||||
return os.path.join(install_dir, path[2:])
|
return os.path.join(install_dir, path[2:])
|
||||||
@ -179,6 +187,7 @@ EOF
|
|||||||
cp "$AGENT_SOURCE" "$AGENT_INSTALL_DIR/rk3588-agent"
|
cp "$AGENT_SOURCE" "$AGENT_INSTALL_DIR/rk3588-agent"
|
||||||
chmod +x "$AGENT_INSTALL_DIR/rk3588-agent"
|
chmod +x "$AGENT_INSTALL_DIR/rk3588-agent"
|
||||||
|
|
||||||
|
# Agent 配置 - 禁用进程管理,改为监控模式
|
||||||
cat > "$AGENT_INSTALL_DIR/agent.config.json" << 'AGENT_CONFIG'
|
cat > "$AGENT_INSTALL_DIR/agent.config.json" << 'AGENT_CONFIG'
|
||||||
{
|
{
|
||||||
"agent": {
|
"agent": {
|
||||||
@ -193,11 +202,11 @@ EOF
|
|||||||
"max_upload_mb": 200,
|
"max_upload_mb": 200,
|
||||||
"config_path": "/opt/rk3588-media-server/etc/media-server.json",
|
"config_path": "/opt/rk3588-media-server/etc/media-server.json",
|
||||||
"media_server_process": {
|
"media_server_process": {
|
||||||
"enable": true,
|
"enable": false,
|
||||||
"exec_path": "/opt/rk3588-media-server/bin/media-server",
|
"exec_path": "",
|
||||||
"work_dir": "/opt/rk3588-media-server",
|
"work_dir": "",
|
||||||
"configs_dir": "/opt/rk3588-media-server/etc",
|
"configs_dir": "",
|
||||||
"pid_file": "/var/run/rk3588sys-media-server.pid",
|
"pid_file": "",
|
||||||
"graceful_timeout_ms": 5000
|
"graceful_timeout_ms": 5000
|
||||||
},
|
},
|
||||||
"media_server_base_url": "http://127.0.0.1:9000",
|
"media_server_base_url": "http://127.0.0.1:9000",
|
||||||
@ -213,10 +222,32 @@ AGENT_CONFIG
|
|||||||
# 安装服务
|
# 安装服务
|
||||||
echo -e "${YELLOW}[3/3] 安装 Systemd 服务...${NC}"
|
echo -e "${YELLOW}[3/3] 安装 Systemd 服务...${NC}"
|
||||||
|
|
||||||
|
# Media Server 独立服务
|
||||||
|
cat > "$SERVICE_DIR/media-server.service" << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=RK3588 Media Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/rk3588-media-server
|
||||||
|
ExecStart=/opt/rk3588-media-server/bin/media-server --config /opt/rk3588-media-server/etc/media-server.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Agent 独立服务
|
||||||
cat > "$SERVICE_DIR/rk3588-agent.service" << 'EOF'
|
cat > "$SERVICE_DIR/rk3588-agent.service" << 'EOF'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=RK3588 Agent Service
|
Description=RK3588 Agent Service
|
||||||
After=network.target
|
After=network.target media-server.service
|
||||||
|
Wants=media-server.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@ -233,10 +264,11 @@ WantedBy=multi-user.target
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
touch /var/run/rk3588sys-media-server.pid
|
|
||||||
chmod 666 /var/run/rk3588sys-media-server.pid
|
|
||||||
|
|
||||||
# 启动 Agent(Agent 会自动管理 Media Server)
|
# 启动服务(独立运行)
|
||||||
|
systemctl enable media-server
|
||||||
|
systemctl start media-server
|
||||||
|
|
||||||
systemctl enable rk3588-agent
|
systemctl enable rk3588-agent
|
||||||
systemctl start rk3588-agent
|
systemctl start rk3588-agent
|
||||||
|
|
||||||
@ -248,9 +280,13 @@ EOF
|
|||||||
echo -e "${GREEN}║ 安装完成! ║${NC}"
|
echo -e "${GREEN}║ 安装完成! ║${NC}"
|
||||||
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "架构说明:"
|
||||||
|
echo " Media Server: 独立 systemd 服务(高可用,崩溃自动重启)"
|
||||||
|
echo " Agent: 独立 systemd 服务(监控状态,Web 管理)"
|
||||||
|
echo ""
|
||||||
echo "管理命令:"
|
echo "管理命令:"
|
||||||
echo " 状态: sudo ./manage.sh status"
|
echo " 状态: sudo ./manage.sh status"
|
||||||
echo " 日志: sudo journalctl -u rk3588-agent -f"
|
echo " 日志: sudo journalctl -u media-server -f"
|
||||||
echo " 界面: http://<设备IP>:9100"
|
echo " 界面: http://<设备IP>:9100"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +307,24 @@ cmd_status() {
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${YELLOW}[Media Server]${NC}"
|
||||||
|
if systemctl is-active --quiet media-server 2>/dev/null; then
|
||||||
|
echo -e " 状态: ${GREEN}● 运行中${NC}"
|
||||||
|
PID=$(systemctl show --property=MainPID --value media-server 2>/dev/null)
|
||||||
|
echo " PID: $PID"
|
||||||
|
if [ -n "$PID" ] && [ "$PID" != "0" ]; then
|
||||||
|
CPU_MEM=$(ps -p $PID -o %cpu,%mem --no-headers 2>/dev/null || echo "N/A")
|
||||||
|
echo " CPU/MEM: $CPU_MEM"
|
||||||
|
fi
|
||||||
|
echo " 端口:"
|
||||||
|
ss -tlnp 2>/dev/null | grep -E "(9000|8554|8555)" | head -3 | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo -e " 状态: ${RED}○ 停止${NC}"
|
||||||
|
echo " 错误日志:"
|
||||||
|
journalctl -u media-server --no-pager -n 3 2>/dev/null | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
echo -e "${YELLOW}[RK3588 Agent]${NC}"
|
echo -e "${YELLOW}[RK3588 Agent]${NC}"
|
||||||
if systemctl is-active --quiet rk3588-agent 2>/dev/null; then
|
if systemctl is-active --quiet rk3588-agent 2>/dev/null; then
|
||||||
echo -e " 状态: ${GREEN}● 运行中${NC}"
|
echo -e " 状态: ${GREEN}● 运行中${NC}"
|
||||||
@ -285,29 +339,14 @@ cmd_status() {
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo -e "${YELLOW}[Media Server]${NC}"
|
echo -e "${YELLOW}[最近日志 - Media Server]${NC}"
|
||||||
# 检查 Media Server 是否由 Agent 启动
|
journalctl -u media-server --no-pager -n 5 2>/dev/null | tail -5 || echo " 无日志"
|
||||||
AGENT_PID=$(systemctl show --property=MainPID --value rk3588-agent 2>/dev/null)
|
|
||||||
MS_PID=$(pgrep -f "media-server" | head -1)
|
|
||||||
if [ -n "$MS_PID" ]; then
|
|
||||||
echo -e " 状态: ${GREEN}● 运行中${NC}"
|
|
||||||
echo " PID: $MS_PID"
|
|
||||||
CPU_MEM=$(ps -p $MS_PID -o %cpu,%mem --no-headers 2>/dev/null || echo "N/A")
|
|
||||||
echo " CPU/MEM: $CPU_MEM"
|
|
||||||
echo " 端口:"
|
|
||||||
ss -tlnp 2>/dev/null | grep -E "(9000|8554|8555)" | head -3 | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo -e " 状态: ${RED}○ 停止${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo -e "${YELLOW}[最近日志]${NC}"
|
|
||||||
journalctl -u rk3588-agent --no-pager -n 5 2>/dev/null | tail -5 || echo " 无日志"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "常用命令:"
|
echo "常用命令:"
|
||||||
echo " 重启: sudo systemctl restart rk3588-agent"
|
echo " 启动: sudo systemctl start media-server"
|
||||||
echo " 日志: sudo journalctl -u rk3588-agent -f"
|
echo " 停止: sudo systemctl stop media-server"
|
||||||
|
echo " 重启: sudo systemctl restart media-server"
|
||||||
echo " 配置: sudo nano /opt/rk3588-media-server/etc/media-server.json"
|
echo " 配置: sudo nano /opt/rk3588-media-server/etc/media-server.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,21 +354,21 @@ cmd_uninstall() {
|
|||||||
echo -e "${YELLOW}========== RK3588 Media Server 卸载 ==========${NC}"
|
echo -e "${YELLOW}========== RK3588 Media Server 卸载 ==========${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
echo -e "${RED}错误: 请使用 sudo 运行${NC}"
|
echo -e "${RED}错误: 请使用 sudo 运行${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -p "确定要卸载并停止所有服务? (y/N): " -n 1 -r
|
read -p "确定要卸载并停止所有服务? (y/N): " -r
|
||||||
echo
|
[ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ] && echo "已取消" && exit 0
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && echo "已取消" && exit 0
|
|
||||||
|
|
||||||
echo "[1/3] 停止服务..."
|
echo "[1/3] 停止服务..."
|
||||||
systemctl stop rk3588-agent 2>/dev/null || true
|
systemctl stop media-server rk3588-agent 2>/dev/null || true
|
||||||
systemctl disable rk3588-agent 2>/dev/null || true
|
systemctl disable media-server rk3588-agent 2>/dev/null || true
|
||||||
echo -e "${GREEN}✓${NC} 服务已停止"
|
echo -e "${GREEN}✓${NC} 服务已停止"
|
||||||
|
|
||||||
echo "[2/3] 删除服务文件..."
|
echo "[2/3] 删除服务文件..."
|
||||||
|
rm -f "$SERVICE_DIR/media-server.service"
|
||||||
rm -f "$SERVICE_DIR/rk3588-agent.service"
|
rm -f "$SERVICE_DIR/rk3588-agent.service"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
echo -e "${GREEN}✓${NC} 服务文件已删除"
|
echo -e "${GREEN}✓${NC} 服务文件已删除"
|
||||||
@ -341,9 +380,8 @@ cmd_uninstall() {
|
|||||||
[ -d "$INSTALL_DIR/models" ] && cp -r "$INSTALL_DIR/models" "$BACKUP_DIR/" 2>/dev/null || true
|
[ -d "$INSTALL_DIR/models" ] && cp -r "$INSTALL_DIR/models" "$BACKUP_DIR/" 2>/dev/null || true
|
||||||
echo -e "${GREEN}✓${NC} 配置已备份到: $BACKUP_DIR"
|
echo -e "${GREEN}✓${NC} 配置已备份到: $BACKUP_DIR"
|
||||||
|
|
||||||
read -p "是否删除安装目录? (y/N): " -n 1 -r
|
read -p "是否删除安装目录? (y/N): " -r
|
||||||
echo
|
if [ "$REPLY" = "y" ] || [ "$REPLY" = "Y" ]; then
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
rm -rf "$INSTALL_DIR" "$AGENT_INSTALL_DIR"
|
rm -rf "$INSTALL_DIR" "$AGENT_INSTALL_DIR"
|
||||||
echo -e "${GREEN}✓${NC} 目录已删除"
|
echo -e "${GREEN}✓${NC} 目录已删除"
|
||||||
else
|
else
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user