فهرست منبع

feat(stream-push): add local video streaming API and UI components

- Introduced API functions for managing local video streams, including starting, stopping, and retrieving stream status.
- Created a new Vue component for local video stream management, featuring a user-friendly interface for controlling streams.
- Updated types to support local video streaming functionality, ensuring consistency across the application.
- Added comprehensive documentation for the local video streaming API to guide users in implementation.
yb 1 هفته پیش
والد
کامیت
da12caaf65
4فایلهای تغییر یافته به همراه1245 افزوده شده و 29 حذف شده
  1. 854 0
      docs/api_torna/stream_push.md
  2. 120 0
      src/api/stream-push.ts
  3. 90 0
      src/types/index.ts
  4. 181 29
      src/views/live-stream/index.vue

+ 854 - 0
docs/api_torna/stream_push.md

@@ -0,0 +1,854 @@
+# 文档
+
+## 推流服务
+
+## 本地视频推流 Controller
+
+本地视频直接推送到 Cloudflare WHIP:
+
+- POST /stream/local/start 启动本地视频推流
+- POST /stream/local/stop 停止本地视频推流
+- GET /stream/local/{name} 获取推流状态
+- GET /stream/local/list 获取所有本地视频推流
+
+### 启动本地视频推流
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `POST` http://localhost:10050/api/stream/local/start
+- 开发环境: `POST` https://tg-live-game.pwtk.cc/api/stream/local/start
+
+描述:启动本地视频推流
+
+ContentType:`application/json`
+
+#### 请求参数
+
+##### Body Parameter
+
+| 名称            | 类型    | 必填 | 最大长度 | 描述                                        | 示例值 |
+| --------------- | ------- | ---- | -------- | ------------------------------------------- | ------ |
+| streamName      | String  | 否   | -        | 流名称(唯一标识)                          |        |
+| videoPath       | String  | 否   | -        | 视频文件路径                                |        |
+| loop            | Boolean | 否   | -        | 是否循环播放                                | true   |
+| targetChannelId | String  | 否   | -        | 目标推流通道 ID(可选,不传则使用默认通道) |        |
+
+#### 请求示例
+
+```
+{
+    "streamName": "string",
+    "videoPath": "string",
+    "loop": true,
+    "targetChannelId": "string"
+}
+```
+
+#### 响应参数
+
+| 名称           | 类型    | 必填 | 最大长度 | 描述                                       | 示例值 |
+| -------------- | ------- | ---- | -------- | ------------------------------------------ | ------ |
+| success        | Boolean | 否   | -        | 请求是否成功                               | true   |
+| errCode        | String  | 否   | -        | 错误码(失败时返回)                       |        |
+| errMessage     | String  | 否   | -        | 错误信息(失败时返回)                     |        |
+| data           | object  | 否   |          | 响应数据 (ActualType: LocalVideoStreamDTO) |        |
+| └ streamName   | String  | 否   | -        | 流名称                                     |        |
+| └ sourceType   | String  | 否   | -        | 源类型:local_video 或 rtsp_camera         |        |
+| └ sourcePath   | String  | 否   | -        | 源路径(视频文件路径或 RTSP URL)          |        |
+| └ rtspUrl      | String  | 否   | -        | MediaMTX 提供的 RTSP URL                   |        |
+| └ loop         | Boolean | 否   | -        | 是否循环播放                               | true   |
+| └ streamTaskId | String  | 否   | -        | 推流任务 ID(如果已推送到 Cloudflare)     |        |
+| └ playbackUrl  | String  | 否   | -        | 播放地址(Cloudflare HLS/WHEP)            |        |
+| └ status       | String  | 否   | -        | 状态                                       |        |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamName": "string",
+        "sourceType": "string",
+        "sourcePath": "string",
+        "rtspUrl": "string",
+        "loop": true,
+        "streamTaskId": "string",
+        "playbackUrl": "string",
+        "status": "string"
+    }
+}
+```
+
+#### 错误码
+
+无
+
+### 停止本地视频推流
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `POST` http://localhost:10050/api/stream/local/stop
+- 开发环境: `POST` https://tg-live-game.pwtk.cc/api/stream/local/stop
+
+描述:停止本地视频推流
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### 请求参数
+
+##### Query Parameter
+
+| 名称       | 类型   | 必填 | 最大长度 | 描述               | 示例值 |
+| ---------- | ------ | ---- | -------- | ------------------ | ------ |
+| streamName | string | 是   | -        | No comments found. |        |
+
+#### 响应参数
+
+| 名称       | 类型    | 必填 | 最大长度 | 描述                        | 示例值 |
+| ---------- | ------- | ---- | -------- | --------------------------- | ------ |
+| success    | Boolean | 否   | -        | 请求是否成功                | true   |
+| errCode    | String  | 否   | -        | 错误码(失败时返回)        |        |
+| errMessage | String  | 否   | -        | 错误信息(失败时返回)      |        |
+| data       | object  | 否   | -        | 响应数据 (ActualType: Void) |        |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {}
+}
+```
+
+#### 错误码
+
+无
+
+### 获取本地视频推流状态
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/local/{streamName}
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/local/{streamName}
+
+描述:获取本地视频推流状态
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### Path 参数
+
+| 名称       | 必填 | 描述               | 示例值 |
+| ---------- | ---- | ------------------ | ------ |
+| streamName | 是   | No comments found. |        |
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称           | 类型    | 必填 | 最大长度 | 描述                                       | 示例值 |
+| -------------- | ------- | ---- | -------- | ------------------------------------------ | ------ |
+| success        | Boolean | 否   | -        | 请求是否成功                               | true   |
+| errCode        | String  | 否   | -        | 错误码(失败时返回)                       |        |
+| errMessage     | String  | 否   | -        | 错误信息(失败时返回)                     |        |
+| data           | object  | 否   |          | 响应数据 (ActualType: LocalVideoStreamDTO) |        |
+| └ streamName   | String  | 否   | -        | 流名称                                     |        |
+| └ sourceType   | String  | 否   | -        | 源类型:local_video 或 rtsp_camera         |        |
+| └ sourcePath   | String  | 否   | -        | 源路径(视频文件路径或 RTSP URL)          |        |
+| └ rtspUrl      | String  | 否   | -        | MediaMTX 提供的 RTSP URL                   |        |
+| └ loop         | Boolean | 否   | -        | 是否循环播放                               | true   |
+| └ streamTaskId | String  | 否   | -        | 推流任务 ID(如果已推送到 Cloudflare)     |        |
+| └ playbackUrl  | String  | 否   | -        | 播放地址(Cloudflare HLS/WHEP)            |        |
+| └ status       | String  | 否   | -        | 状态                                       |        |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamName": "string",
+        "sourceType": "string",
+        "sourcePath": "string",
+        "rtspUrl": "string",
+        "loop": true,
+        "streamTaskId": "string",
+        "playbackUrl": "string",
+        "status": "string"
+    }
+}
+```
+
+#### 错误码
+
+无
+
+### 获取所有本地视频推流
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/local/list
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/local/list
+
+描述:获取所有本地视频推流
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称           | 类型    | 必填 | 最大长度 | 描述                                   | 示例值 |
+| -------------- | ------- | ---- | -------- | -------------------------------------- | ------ |
+| success        | Boolean | 否   | -        | 请求是否成功                           | true   |
+| errCode        | String  | 否   | -        | 错误码(失败时返回)                   |        |
+| errMessage     | String  | 否   | -        | 错误信息(失败时返回)                 |        |
+| data           | array   | 否   |          | 响应数据 (ActualType: List)            |        |
+| └ streamName   | String  | 否   | -        | 流名称                                 |        |
+| └ sourceType   | String  | 否   | -        | 源类型:local_video 或 rtsp_camera     |        |
+| └ sourcePath   | String  | 否   | -        | 源路径(视频文件路径或 RTSP URL)      |        |
+| └ rtspUrl      | String  | 否   | -        | MediaMTX 提供的 RTSP URL               |        |
+| └ loop         | Boolean | 否   | -        | 是否循环播放                           | true   |
+| └ streamTaskId | String  | 否   | -        | 推流任务 ID(如果已推送到 Cloudflare) |        |
+| └ playbackUrl  | String  | 否   | -        | 播放地址(Cloudflare HLS/WHEP)        |        |
+| └ status       | String  | 否   | -        | 状态                                   |        |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": [
+        {
+            "streamName": "string",
+            "sourceType": "string",
+            "sourcePath": "string",
+            "rtspUrl": "string",
+            "loop": true,
+            "streamTaskId": "string",
+            "playbackUrl": "string",
+            "status": "string"
+        }
+    ]
+}
+```
+
+#### 错误码
+
+无
+
+## 推流服务 Controller
+
+推流管理 API 接口:
+
+- POST /stream/start 启动推流任务
+- POST /stream/stop 停止推流任务
+- GET /stream/task/{streamSn} 获取任务状态
+- GET /stream/tasks 获取 LSS 推流任务列表
+- GET /stream/tasks/active 获取所有活动任务
+- GET /stream/channels 获取推流通道列表
+- POST /stream/switch 切换推流源
+
+### 启动推流任务
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `POST` http://localhost:10050/api/stream/start
+- 开发环境: `POST` https://tg-live-game.pwtk.cc/api/stream/start
+
+描述:启动推流任务
+
+ContentType:`application/json`
+
+#### 请求参数
+
+##### Body Parameter
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| lssId | String | 否 | - | LSS 节点 ID(可选,不传则自动选择) | lss_001 |
+| cameraId | String | 否 | - | 摄像头 ID(必填) | cam_001 |
+| sourceRtspUrl | String | 否 | - | 源 RTSP 地址(可选,如不传则从摄像头服务获取) | rtsp://admin:password@192.168.1.101:554/stream1 |
+| profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| remark | String | 否 | - | 备注 |  |
+
+#### 请求示例
+
+```
+{
+    "name": "投币机1号直播",
+    "lssId": "lss_001",
+    "cameraId": "cam_001",
+    "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+    "profile": "low_latency",
+    "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+    "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+    "remark": "string"
+}
+```
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | object | 否 |  | 响应数据 (ActualType: StreamTaskDTO) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ lssId | String | 否 | - | LSS 节点 ID | lss_001 |
+| └ cameraId | String | 否 | - | 摄像头 ID | cam_001 |
+| └ sourceRtspUrl | String | 否 | - | 源 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+| └ profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| └ whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| └ playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| └ status | String | 否 | - | 推流状态: IDLE, STARTING, STREAMING, STOPPED, ERROR | STREAMING |
+| └ statusDescription | String | 否 | - | 状态描述 | 推流中 |
+| └ errorMessage | String | 否 | - | 错误信息 |  |
+| └ retryCount | int | 否 | - | 重试次数 | 0 |
+| └ remark | String | 否 | - | 备注 |  |
+| └ createdAt | LocalDateTime | 否 | - | 创建时间 | 2024-01-15T10:30:00 |
+| └ startedAt | LocalDateTime | 否 | - | 开始推流时间 | 2024-01-15T10:30:05 |
+| └ stoppedAt | LocalDateTime | 否 | - | 停止推流时间 | yyyy-MM-dd HH:mm:ss |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamSn": "stream_abc123def456",
+        "name": "投币机1号直播",
+        "lssId": "lss_001",
+        "cameraId": "cam_001",
+        "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+        "profile": "low_latency",
+        "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+        "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+        "status": "STREAMING",
+        "statusDescription": "推流中",
+        "errorMessage": "string",
+        "retryCount": 0,
+        "remark": "string",
+        "createdAt": "2024-01-15T10:30:00",
+        "startedAt": "2024-01-15T10:30:05",
+        "stoppedAt": "yyyy-MM-dd HH:mm:ss"
+    }
+}
+```
+
+#### 错误码
+
+无
+
+### 停止推流任务
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `POST` http://localhost:10050/api/stream/stop
+- 开发环境: `POST` https://tg-live-game.pwtk.cc/api/stream/stop
+
+描述:停止推流任务
+
+ContentType:`application/json`
+
+#### 请求参数
+
+##### Body Parameter
+
+| 名称   | 类型   | 必填 | 最大长度 | 描述                                                 | 示例值              |
+| ------ | ------ | ---- | -------- | ---------------------------------------------------- | ------------------- |
+| taskId | String | 否   | -        | 推流任务流水号(与 lssId 二选一)                    | stream_abc123def456 |
+| lssId  | String | 否   | -        | LSS 节点 ID(与 taskId 二选一,停止该 LSS 所有推流) | lss_001             |
+
+#### 请求示例
+
+```
+{
+    "taskId": "stream_abc123def456",
+    "lssId": "lss_001"
+}
+```
+
+#### 响应参数
+
+| 名称       | 类型    | 必填 | 最大长度 | 描述                        | 示例值 |
+| ---------- | ------- | ---- | -------- | --------------------------- | ------ |
+| success    | Boolean | 否   | -        | 请求是否成功                | true   |
+| errCode    | String  | 否   | -        | 错误码(失败时返回)        |        |
+| errMessage | String  | 否   | -        | 错误信息(失败时返回)      |        |
+| data       | object  | 否   | -        | 响应数据 (ActualType: Void) |        |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {}
+}
+```
+
+#### 错误码
+
+无
+
+### 获取任务状态
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/task/{streamSn}
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/task/{streamSn}
+
+描述:获取任务状态
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### Path 参数
+
+| 名称     | 必填 | 描述           | 示例值              |
+| -------- | ---- | -------------- | ------------------- |
+| streamSn | 是   | 推流任务流水号 | stream_abc123def456 |
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | object | 否 |  | 响应数据 (ActualType: StreamTaskDTO) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ lssId | String | 否 | - | LSS 节点 ID | lss_001 |
+| └ cameraId | String | 否 | - | 摄像头 ID | cam_001 |
+| └ sourceRtspUrl | String | 否 | - | 源 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+| └ profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| └ whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| └ playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| └ status | String | 否 | - | 推流状态: IDLE, STARTING, STREAMING, STOPPED, ERROR | STREAMING |
+| └ statusDescription | String | 否 | - | 状态描述 | 推流中 |
+| └ errorMessage | String | 否 | - | 错误信息 |  |
+| └ retryCount | int | 否 | - | 重试次数 | 0 |
+| └ remark | String | 否 | - | 备注 |  |
+| └ createdAt | LocalDateTime | 否 | - | 创建时间 | 2024-01-15T10:30:00 |
+| └ startedAt | LocalDateTime | 否 | - | 开始推流时间 | 2024-01-15T10:30:05 |
+| └ stoppedAt | LocalDateTime | 否 | - | 停止推流时间 | yyyy-MM-dd HH:mm:ss |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamSn": "stream_abc123def456",
+        "name": "投币机1号直播",
+        "lssId": "lss_001",
+        "cameraId": "cam_001",
+        "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+        "profile": "low_latency",
+        "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+        "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+        "status": "STREAMING",
+        "statusDescription": "推流中",
+        "errorMessage": "string",
+        "retryCount": 0,
+        "remark": "string",
+        "createdAt": "2024-01-15T10:30:00",
+        "startedAt": "2024-01-15T10:30:05",
+        "stoppedAt": "yyyy-MM-dd HH:mm:ss"
+    }
+}
+```
+
+#### 错误码
+
+无
+
+### 获取 LSS 推流任务列表
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/tasks
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/tasks
+
+描述:获取 LSS 推流任务列表
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### 请求参数
+
+##### Query Parameter
+
+| 名称  | 类型   | 必填 | 最大长度 | 描述        | 示例值  |
+| ----- | ------ | ---- | -------- | ----------- | ------- |
+| lssId | string | 是   | -        | LSS 节点 ID | lss_001 |
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | array | 否 |  | 响应数据 (ActualType: List) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ lssId | String | 否 | - | LSS 节点 ID | lss_001 |
+| └ cameraId | String | 否 | - | 摄像头 ID | cam_001 |
+| └ sourceRtspUrl | String | 否 | - | 源 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+| └ profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| └ whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| └ playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| └ status | String | 否 | - | 推流状态: IDLE, STARTING, STREAMING, STOPPED, ERROR | STREAMING |
+| └ statusDescription | String | 否 | - | 状态描述 | 推流中 |
+| └ errorMessage | String | 否 | - | 错误信息 |  |
+| └ retryCount | int | 否 | - | 重试次数 | 0 |
+| └ remark | String | 否 | - | 备注 |  |
+| └ createdAt | LocalDateTime | 否 | - | 创建时间 | 2024-01-15T10:30:00 |
+| └ startedAt | LocalDateTime | 否 | - | 开始推流时间 | 2024-01-15T10:30:05 |
+| └ stoppedAt | LocalDateTime | 否 | - | 停止推流时间 | yyyy-MM-dd HH:mm:ss |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": [
+        {
+            "streamSn": "stream_abc123def456",
+            "name": "投币机1号直播",
+            "lssId": "lss_001",
+            "cameraId": "cam_001",
+            "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+            "profile": "low_latency",
+            "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+            "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+            "status": "STREAMING",
+            "statusDescription": "推流中",
+            "errorMessage": "string",
+            "retryCount": 0,
+            "remark": "string",
+            "createdAt": "2024-01-15T10:30:00",
+            "startedAt": "2024-01-15T10:30:05",
+            "stoppedAt": "yyyy-MM-dd HH:mm:ss"
+        }
+    ]
+}
+```
+
+#### 错误码
+
+无
+
+### 获取所有活动任务
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/tasks/active
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/tasks/active
+
+描述:获取所有活动任务
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | array | 否 |  | 响应数据 (ActualType: List) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ lssId | String | 否 | - | LSS 节点 ID | lss_001 |
+| └ cameraId | String | 否 | - | 摄像头 ID | cam_001 |
+| └ sourceRtspUrl | String | 否 | - | 源 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+| └ profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| └ whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| └ playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| └ status | String | 否 | - | 推流状态: IDLE, STARTING, STREAMING, STOPPED, ERROR | STREAMING |
+| └ statusDescription | String | 否 | - | 状态描述 | 推流中 |
+| └ errorMessage | String | 否 | - | 错误信息 |  |
+| └ retryCount | int | 否 | - | 重试次数 | 0 |
+| └ remark | String | 否 | - | 备注 |  |
+| └ createdAt | LocalDateTime | 否 | - | 创建时间 | 2024-01-15T10:30:00 |
+| └ startedAt | LocalDateTime | 否 | - | 开始推流时间 | 2024-01-15T10:30:05 |
+| └ stoppedAt | LocalDateTime | 否 | - | 停止推流时间 | yyyy-MM-dd HH:mm:ss |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": [
+        {
+            "streamSn": "stream_abc123def456",
+            "name": "投币机1号直播",
+            "lssId": "lss_001",
+            "cameraId": "cam_001",
+            "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+            "profile": "low_latency",
+            "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+            "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+            "status": "STREAMING",
+            "statusDescription": "推流中",
+            "errorMessage": "string",
+            "retryCount": 0,
+            "remark": "string",
+            "createdAt": "2024-01-15T10:30:00",
+            "startedAt": "2024-01-15T10:30:05",
+            "stoppedAt": "yyyy-MM-dd HH:mm:ss"
+        }
+    ]
+}
+```
+
+#### 错误码
+
+无
+
+### 获取推流通道列表
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/channels
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/channels
+
+描述:获取推流通道列表
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | array | 否 |  | 响应数据 (ActualType: List) |  |
+| └ channelId | String | 否 | - | 通道 ID | cf_channel_001 |
+| └ name | String | 否 | - | 通道名称 | 主推流通道 |
+| └ mode | String | 否 | - | 推流模式: WHIP, RTMPS | WHIP |
+| └ hlsPlaybackUrl | String | 否 | - | HLS 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/manifest/video.m3u8 |
+| └ recordingEnabled | boolean | 否 | - | 是否启用录制 | false |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": [
+        {
+            "channelId": "cf_channel_001",
+            "name": "主推流通道",
+            "mode": "WHIP",
+            "hlsPlaybackUrl": "https://customer-xxx.cloudflarestream.com/xxx/manifest/video.m3u8",
+            "recordingEnabled": false
+        }
+    ]
+}
+```
+
+#### 错误码
+
+无
+
+### 获取播放信息
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `GET` http://localhost:10050/api/stream/playback/{streamSn}
+- 开发环境: `GET` https://tg-live-game.pwtk.cc/api/stream/playback/{streamSn}
+
+描述:获取播放信息
+
+ContentType:`application/x-www-form-urlencoded;charset=UTF-8`
+
+#### Path 参数
+
+| 名称     | 必填 | 描述           | 示例值              |
+| -------- | ---- | -------------- | ------------------- |
+| streamSn | 是   | 推流任务流水号 | stream_abc123def456 |
+
+#### 请求参数
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | object | 否 |  | 响应数据 (ActualType: PlaybackInfoDTO) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ status | String | 否 | - | 推流状态 | STREAMING |
+| └ whepUrl | String | 否 | - | WHEP 播放地址(WebRTC 低延迟) | https://customer-pj89kn2ke2tcuh19.cloudflarestream.com/xxx/webRTC/play |
+| └ hlsUrl | String | 否 | - | HLS 播放地址 | https://customer-pj89kn2ke2tcuh19.cloudflarestream.com/xxx/manifest/video.m3u8 |
+| └ iframeCode | String | 否 | - | iframe 嵌入代码 | <iframe src=... allow=accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture; allowfullscreen=true></iframe> |
+| └ isLive | Boolean | 否 | - | 是否正在推流 | true |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamSn": "stream_abc123def456",
+        "name": "投币机1号直播",
+        "status": "STREAMING",
+        "whepUrl": "https://customer-pj89kn2ke2tcuh19.cloudflarestream.com/xxx/webRTC/play",
+        "hlsUrl": "https://customer-pj89kn2ke2tcuh19.cloudflarestream.com/xxx/manifest/video.m3u8",
+        "iframeCode": "<iframe src=... allow=accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture; allowfullscreen=true></iframe>",
+        "isLive": true
+    }
+}
+```
+
+#### 错误码
+
+无
+
+### 切换推流源
+
+维护人:TG Live
+
+#### URL
+
+- 本地开发环境: `POST` http://localhost:10050/api/stream/switch
+- 开发环境: `POST` https://tg-live-game.pwtk.cc/api/stream/switch
+
+描述:切换推流源
+
+ContentType:`application/json`
+
+#### 请求参数
+
+##### Body Parameter
+
+| 名称        | 类型   | 必填 | 最大长度 | 描述           | 示例值                                          |
+| ----------- | ------ | ---- | -------- | -------------- | ----------------------------------------------- |
+| streamSn    | String | 是   | -        | 推流任务流水号 | stream_abc123def456                             |
+| newCameraId | String | 是   | -        | 新的摄像头 ID  | cam_001                                         |
+| newRtspUrl  | String | 是   | -        | 新的 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+
+#### 请求示例
+
+```
+{
+    "streamSn": "stream_abc123def456",
+    "newCameraId": "cam_001",
+    "newRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1"
+}
+```
+
+#### 响应参数
+
+| 名称 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
+| --- | --- | --- | --- | --- | --- |
+| success | Boolean | 否 | - | 请求是否成功 | true |
+| errCode | String | 否 | - | 错误码(失败时返回) |  |
+| errMessage | String | 否 | - | 错误信息(失败时返回) |  |
+| data | object | 否 |  | 响应数据 (ActualType: StreamTaskDTO) |  |
+| └ streamSn | String | 否 | - | 推流任务流水号 | stream_abc123def456 |
+| └ name | String | 否 | - | 任务名称 | 投币机 1 号直播 |
+| └ lssId | String | 否 | - | LSS 节点 ID | lss_001 |
+| └ cameraId | String | 否 | - | 摄像头 ID | cam_001 |
+| └ sourceRtspUrl | String | 否 | - | 源 RTSP 地址 | rtsp://admin:password@192.168.1.101:554/stream1 |
+| └ profile | String | 否 | - | 推流配置档位: low_latency / standard / file_loop | low_latency |
+| └ whipUrl | String | 否 | - | WHIP 推流地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish |
+| └ playbackUrl | String | 否 | - | WebRTC 播放地址 | https://customer-xxx.cloudflarestream.com/xxx/webRTC/play |
+| └ status | String | 否 | - | 推流状态: IDLE, STARTING, STREAMING, STOPPED, ERROR | STREAMING |
+| └ statusDescription | String | 否 | - | 状态描述 | 推流中 |
+| └ errorMessage | String | 否 | - | 错误信息 |  |
+| └ retryCount | int | 否 | - | 重试次数 | 0 |
+| └ remark | String | 否 | - | 备注 |  |
+| └ createdAt | LocalDateTime | 否 | - | 创建时间 | 2024-01-15T10:30:00 |
+| └ startedAt | LocalDateTime | 否 | - | 开始推流时间 | 2024-01-15T10:30:05 |
+| └ stoppedAt | LocalDateTime | 否 | - | 停止推流时间 | yyyy-MM-dd HH:mm:ss |
+
+#### 响应示例
+
+```
+{
+    "success": true,
+    "errCode": "string",
+    "errMessage": "string",
+    "data": {
+        "streamSn": "stream_abc123def456",
+        "name": "投币机1号直播",
+        "lssId": "lss_001",
+        "cameraId": "cam_001",
+        "sourceRtspUrl": "rtsp://admin:password@192.168.1.101:554/stream1",
+        "profile": "low_latency",
+        "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/publish",
+        "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/webRTC/play",
+        "status": "STREAMING",
+        "statusDescription": "推流中",
+        "errorMessage": "string",
+        "retryCount": 0,
+        "remark": "string",
+        "createdAt": "2024-01-15T10:30:00",
+        "startedAt": "2024-01-15T10:30:05",
+        "stoppedAt": "yyyy-MM-dd HH:mm:ss"
+    }
+}
+```
+
+#### 错误码
+
+无

+ 120 - 0
src/api/stream-push.ts

@@ -0,0 +1,120 @@
+/**
+ * 推流服务 API
+ *
+ * 本地视频推流 Controller:
+ * - POST /stream/local/start       启动本地视频推流
+ * - POST /stream/local/stop        停止本地视频推流
+ * - GET  /stream/local/{name}      获取推流状态
+ * - GET  /stream/local/list        获取所有本地视频推流
+ *
+ * 推流服务 Controller:
+ * - POST /stream/start             启动推流任务
+ * - POST /stream/stop              停止推流任务
+ * - GET  /stream/task/{streamSn}   获取任务状态
+ * - GET  /stream/tasks             获取 LSS 推流任务列表
+ * - GET  /stream/tasks/active      获取所有活动任务
+ * - GET  /stream/channels          获取推流通道列表
+ * - GET  /stream/playback/{streamSn} 获取播放信息
+ * - POST /stream/switch            切换推流源
+ */
+
+import { get, post } from '@/utils/request'
+import type { IBaseResponse, IListResponse } from '@/types'
+import type {
+  LocalVideoStreamDTO,
+  StartLocalStreamRequest,
+  StreamTaskDTO,
+  StartStreamTaskRequest,
+  StopStreamTaskRequest,
+  SwitchStreamSourceRequest,
+  StreamPushChannelDTO,
+  PlaybackInfoDTO
+} from '@/types'
+
+// ==================== 本地视频推流 ====================
+
+/**
+ * 启动本地视频推流
+ */
+export function startLocalStream(data: StartLocalStreamRequest): Promise<IBaseResponse<LocalVideoStreamDTO>> {
+  return post('/stream/local/start', data)
+}
+
+/**
+ * 停止本地视频推流
+ */
+export function stopLocalStream(streamName: string): Promise<IBaseResponse<void>> {
+  return post('/stream/local/stop', null, { params: { streamName } })
+}
+
+/**
+ * 获取本地视频推流状态
+ */
+export function getLocalStreamStatus(streamName: string): Promise<IBaseResponse<LocalVideoStreamDTO>> {
+  return get(`/stream/local/${streamName}`)
+}
+
+/**
+ * 获取所有本地视频推流
+ */
+export function listLocalStreams(): Promise<IListResponse<LocalVideoStreamDTO>> {
+  return get('/stream/local/list')
+}
+
+// ==================== 推流服务 ====================
+
+/**
+ * 启动推流任务
+ */
+export function startStreamTask(data: StartStreamTaskRequest): Promise<IBaseResponse<StreamTaskDTO>> {
+  return post('/stream/start', data)
+}
+
+/**
+ * 停止推流任务
+ */
+export function stopStreamTask(data: StopStreamTaskRequest): Promise<IBaseResponse<void>> {
+  return post('/stream/stop', data)
+}
+
+/**
+ * 获取任务状态
+ */
+export function getStreamTaskStatus(streamSn: string): Promise<IBaseResponse<StreamTaskDTO>> {
+  return get(`/stream/task/${streamSn}`)
+}
+
+/**
+ * 获取 LSS 推流任务列表
+ */
+export function listLssStreamTasks(lssId: string): Promise<IListResponse<StreamTaskDTO>> {
+  return get('/stream/tasks', { lssId })
+}
+
+/**
+ * 获取所有活动任务
+ */
+export function listActiveStreamTasks(): Promise<IListResponse<StreamTaskDTO>> {
+  return get('/stream/tasks/active')
+}
+
+/**
+ * 获取推流通道列表
+ */
+export function listStreamPushChannels(): Promise<IListResponse<StreamPushChannelDTO>> {
+  return get('/stream/channels')
+}
+
+/**
+ * 获取播放信息
+ */
+export function getStreamPlayback(streamSn: string): Promise<IBaseResponse<PlaybackInfoDTO>> {
+  return get(`/stream/playback/${streamSn}`)
+}
+
+/**
+ * 切换推流源
+ */
+export function switchStreamSource(data: SwitchStreamSourceRequest): Promise<IBaseResponse<StreamTaskDTO>> {
+  return post('/stream/switch', data)
+}

+ 90 - 0
src/types/index.ts

@@ -586,3 +586,93 @@ export interface CameraVendorUpdateRequest {
   enabled?: boolean
   sortOrder?: number
 }
+
+// ==================== 推流服务相关类型 (Stream Push) ====================
+
+// 本地视频推流 DTO
+export interface LocalVideoStreamDTO {
+  streamName: string
+  sourceType: 'local_video' | 'rtsp_camera'
+  sourcePath: string
+  rtspUrl?: string
+  loop: boolean
+  streamTaskId?: string
+  playbackUrl?: string
+  status: string
+}
+
+// 启动本地视频推流请求
+export interface StartLocalStreamRequest {
+  streamName?: string
+  videoPath?: string
+  loop?: boolean
+  targetChannelId?: string
+}
+
+// 推流任务状态
+export type StreamTaskStatus = 'IDLE' | 'STARTING' | 'STREAMING' | 'STOPPED' | 'ERROR'
+
+// 推流任务 DTO
+export interface StreamTaskDTO {
+  streamSn: string
+  name: string
+  lssId: string
+  cameraId: string
+  sourceRtspUrl: string
+  profile: 'low_latency' | 'standard' | 'file_loop'
+  whipUrl: string
+  playbackUrl: string
+  status: StreamTaskStatus
+  statusDescription?: string
+  errorMessage?: string
+  retryCount: number
+  remark?: string
+  createdAt: string
+  startedAt?: string
+  stoppedAt?: string
+}
+
+// 启动推流任务请求
+export interface StartStreamTaskRequest {
+  name?: string
+  lssId?: string
+  cameraId: string
+  sourceRtspUrl?: string
+  profile?: 'low_latency' | 'standard' | 'file_loop'
+  whipUrl?: string
+  playbackUrl?: string
+  remark?: string
+}
+
+// 停止推流任务请求
+export interface StopStreamTaskRequest {
+  taskId?: string
+  lssId?: string
+}
+
+// 切换推流源请求
+export interface SwitchStreamSourceRequest {
+  streamSn: string
+  newCameraId: string
+  newRtspUrl: string
+}
+
+// 推流通道信息 DTO (推流服务用)
+export interface StreamPushChannelDTO {
+  channelId: string
+  name: string
+  mode: 'WHIP' | 'RTMPS'
+  hlsPlaybackUrl?: string
+  recordingEnabled: boolean
+}
+
+// 播放信息 DTO
+export interface PlaybackInfoDTO {
+  streamSn: string
+  name: string
+  status: StreamTaskStatus
+  whepUrl?: string
+  hlsUrl?: string
+  iframeCode?: string
+  isLive: boolean
+}

+ 181 - 29
src/views/live-stream/index.vue

@@ -57,6 +57,27 @@
             <span>{{ row.channelId ?? '-' }}</span>
           </template>
         </el-table-column>
+        <el-table-column prop="commandTemplate" :label="t('命令模板')" width="100" align="center">
+          <template #default="{ row }">
+            <el-link type="primary" @click="openCommandDialog(row)">{{ t('查看') }}</el-link>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('推流控制')" width="120" align="center">
+          <template #default="{ row }">
+            <el-button
+              v-if="row.status !== 'STREAMING'"
+              type="success"
+              size="small"
+              :loading="row._starting"
+              @click="handleStartStream(row)"
+            >
+              {{ t('启动') }}
+            </el-button>
+            <el-button v-else type="danger" size="small" :loading="row._stopping" @click="handleStopStream(row)">
+              {{ t('停止') }}
+            </el-button>
+          </template>
+        </el-table-column>
         <el-table-column prop="pushMethod" :label="t('推流方式')" width="100" align="center">
           <template #default="{ row }">
             <el-tag size="small">{{ row.pushMethod || 'ffmpeg' }}</el-tag>
@@ -116,7 +137,7 @@
       <div class="drawer-content">
         <div class="drawer-header">{{ drawerTitle }}</div>
         <div class="drawer-body">
-          <el-form ref="formRef" :model="form" :rules="rules" label-position="left" class="stream-form">
+          <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
             <el-form-item label="名称:" prop="name">
               <el-input v-model="form.name" placeholder="例如: 测试推流-001" style="width: 300px" />
             </el-form-item>
@@ -141,20 +162,14 @@
               </el-select>
             </el-form-item>
             <el-form-item label="推流方式:" prop="pushMethod">
-              <el-select v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
+              <el-select disabled v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
                 <el-option label="ffmpeg" value="ffmpeg" />
               </el-select>
             </el-form-item>
-            <el-form-item label="超时时间:" prop="timeoutSeconds">
-              <el-input-number
-                v-model="form.timeoutSeconds"
-                :min="1"
-                :max="300"
-                placeholder="秒"
-                style="width: 150px"
-              />
+            <!-- <el-form-item label="超时时间:" prop="timeoutSeconds">
+              <el-input-number v-model="form.timeoutSeconds" :min="1" :max="300" placeholder="秒" style="width: 150px" />
               <span style="margin-left: 8px; color: #909399">秒</span>
-            </el-form-item>
+            </el-form-item> -->
             <el-form-item label="命令模板:" prop="commandTemplate">
               <div class="textarea-wrapper">
                 <el-input
@@ -167,16 +182,10 @@
                 />
               </div>
             </el-form-item>
-            <el-form-item label="备注:" prop="remark">
-              <el-input
-                v-model="form.remark"
-                type="textarea"
-                :rows="2"
-                placeholder="备注信息"
-                maxlength="500"
-                style="width: 300px"
-              />
-            </el-form-item>
+            <!-- <el-form-item label="备注:" prop="remark">
+              <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" maxlength="500"
+                style="width: 300px" />
+            </el-form-item> -->
             <el-form-item v-if="isEdit" label="启用状态:" prop="enabled">
               <el-switch v-model="form.enabled" />
             </el-form-item>
@@ -190,17 +199,38 @@
         </div>
       </div>
     </el-drawer>
+
+    <!-- 命令模板查看/编辑弹窗 -->
+    <el-dialog v-model="commandDialogVisible" :title="t('命令模板')" width="700px" destroy-on-close>
+      <div class="command-content">
+        <el-input
+          v-model="currentCommandTemplate"
+          type="textarea"
+          :rows="12"
+          :placeholder="t('请输入 FFmpeg 命令模板')"
+          maxlength="2000"
+          show-word-limit
+        />
+      </div>
+      <template #footer>
+        <el-button @click="commandDialogVisible = false">{{ t('关闭') }}</el-button>
+        <el-button type="primary" :loading="commandUpdateLoading" @click="handleUpdateCommandTemplate">
+          {{ t('更新') }}
+        </el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, computed } from 'vue'
+import { ref, reactive, onMounted, computed, watch } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
 import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
 import { listLiveStreams, addLiveStream, updateLiveStream, deleteLiveStream } from '@/api/live-stream'
 import { listAllLssNodes } from '@/api/lss'
 import { adminListCameras } from '@/api/camera'
 import { listAllStreamChannels } from '@/api/stream-channel'
+import { startStreamTask, stopStreamTask } from '@/api/stream-push'
 import type { LiveStreamDTO, LiveStreamStatus, LssNodeDTO, CameraInfoDTO, StreamChannelDTO } from '@/types'
 import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
@@ -251,6 +281,12 @@ const streamList = ref<LiveStreamDTO[]>([])
 const drawerVisible = ref(false)
 const formRef = ref<FormInstance>()
 
+// 命令模板弹窗
+const commandDialogVisible = ref(false)
+const currentCommandTemplate = ref('')
+const currentStreamId = ref<number | null>(null)
+const commandUpdateLoading = ref(false)
+
 // 下拉选项
 const lssOptions = ref<LssNodeDTO[]>([])
 const cameraOptions = ref<CameraInfoDTO[]>([])
@@ -351,13 +387,26 @@ async function loadOptions() {
   }
 }
 
-//  // 获取摄像头列表 ,
-async function loadCameraOptions() {
-  const cameraRes = await adminListCameras({ size: 1000 })
-  // if (cameraRes.success && cameraRes.data) {
-  //   cameraOptions.value = cameraRes.data || []
-  // }
-}
+// 监听 LSS 节点变化,加载对应的摄像头列表
+watch(
+  () => form.lssId,
+  async (newLssId) => {
+    // 清空当前选中的摄像头
+    form.cameraId = ''
+    cameraOptions.value = []
+
+    if (newLssId) {
+      try {
+        const res = await adminListCameras({ lssId: newLssId, size: 1000 })
+        if (res.success && res.data) {
+          cameraOptions.value = res.data.list || []
+        }
+      } catch (error) {
+        console.error('加载摄像头列表失败', error)
+      }
+    }
+  }
+)
 
 function handleSearch() {
   currentPage.value = 1
@@ -484,6 +533,93 @@ async function handleSubmit() {
   })
 }
 
+// 打开命令模板弹窗
+function openCommandDialog(row: LiveStreamDTO) {
+  currentStreamId.value = row.id
+  currentCommandTemplate.value = row.commandTemplate || ''
+  commandDialogVisible.value = true
+}
+
+// 更新命令模板
+async function handleUpdateCommandTemplate() {
+  if (!currentStreamId.value) return
+
+  commandUpdateLoading.value = true
+  try {
+    const res = await updateLiveStream({
+      id: currentStreamId.value,
+      commandTemplate: currentCommandTemplate.value
+    })
+    if (res.success) {
+      ElMessage.success(t('更新成功'))
+      commandDialogVisible.value = false
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('更新失败'))
+    }
+  } catch (error) {
+    console.error('更新命令模板失败', error)
+    ElMessage.error(t('更新失败'))
+  } finally {
+    commandUpdateLoading.value = false
+  }
+}
+
+// 启动推流
+async function handleStartStream(row: LiveStreamDTO) {
+  if (!row.cameraId) {
+    ElMessage.warning(t('请先配置摄像头'))
+    return
+  }
+
+  row._starting = true
+  try {
+    const res = await startStreamTask({
+      name: row.name,
+      lssId: row.lssId,
+      cameraId: row.cameraId
+    })
+    if (res.success) {
+      ElMessage.success(t('推流任务已启动'))
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('启动失败'))
+    }
+  } catch (error) {
+    console.error('启动推流失败', error)
+    ElMessage.error(t('启动推流失败'))
+  } finally {
+    row._starting = false
+  }
+}
+
+// 停止推流
+async function handleStopStream(row: LiveStreamDTO) {
+  try {
+    await ElMessageBox.confirm(t('确定要停止该推流任务吗?'), t('提示'), {
+      type: 'warning',
+      confirmButtonText: t('确定'),
+      cancelButtonText: t('取消')
+    })
+
+    row._stopping = true
+    const res = await stopStreamTask({ taskId: row.streamSn })
+    if (res.success) {
+      ElMessage.success(t('推流任务已停止'))
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('停止失败'))
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('停止推流失败', error)
+      ElMessage.error(t('停止推流失败'))
+    }
+  } finally {
+    row._stopping = false
+  }
+}
+
 function handleSizeChange(val: number) {
   pageSize.value = val
   currentPage.value = 1
@@ -671,4 +807,20 @@ onMounted(() => {
     }
   }
 }
+
+// 命令模板弹窗样式
+.command-content {
+  :deep(.el-textarea__inner) {
+    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
+    font-size: 13px;
+    line-height: 1.6;
+    background-color: #1e1e1e;
+    color: #d4d4d4;
+    border-radius: 6px;
+
+    &:focus {
+      border-color: #4f46e5;
+    }
+  }
+}
 </style>