From 214479e0abfc8dbbbbb8b1e5ca23eb7e1d3519de Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Sun, 21 Dec 2025 18:55:17 +0800 Subject: [PATCH] WIP: unify live room api + android client updates --- .idea/.gitignore | 8 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - .idea/zhibo.iml | 9 - .kiro/specs/live-streaming-system/design.md | 353 ------------------ .../live-streaming-system/requirements.md | 89 ----- .kiro/specs/live-streaming-system/tasks.md | 66 ---- .vscode/settings.json | 2 - .../front/controller/LiveRoomController.java | 6 +- .../front/response/live/LiveRoomResponse.java | 2 +- android-app/app/build.gradle.kts | 4 +- .../example/livestreaming/MainActivity.java | 15 +- .../livestreaming/RoomDetailActivity.java | 12 +- .../example/livestreaming/net/ApiClient.java | 24 ++ .../livestreaming/net/ApiResponse.java | 19 +- .../example/livestreaming/net/ApiService.java | 11 +- .../example/livestreaming/net/AuthStore.java | 36 ++ .../livestreaming/net/LoginRequest.java | 25 ++ .../livestreaming/net/LoginResponse.java | 13 + 19 files changed, 145 insertions(+), 563 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/zhibo.iml delete mode 100644 .kiro/specs/live-streaming-system/design.md delete mode 100644 .kiro/specs/live-streaming-system/requirements.md delete mode 100644 .kiro/specs/live-streaming-system/tasks.md delete mode 100644 .vscode/settings.json create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/LoginRequest.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410cac..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e3ea9106..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/zhibo.iml b/.idea/zhibo.iml deleted file mode 100644 index d6ebd480..00000000 --- a/.idea/zhibo.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.kiro/specs/live-streaming-system/design.md b/.kiro/specs/live-streaming-system/design.md deleted file mode 100644 index a1ce2591..00000000 --- a/.kiro/specs/live-streaming-system/design.md +++ /dev/null @@ -1,353 +0,0 @@ -# Design Document - -## Overview - -本设计文档描述了基于 SRS 的个人直播系统的技术架构和实现方案。系统采用前后端分离架构,使用 SRS 作为核心流媒体服务器处理视频流的接收和分发,Node.js 后端提供业务 API,React 前端提供用户界面。 - -### 技术栈选型 - -| 组件 | 技术选择 | 理由 | -|------|----------|------| -| 流媒体服务器 | SRS 5.0 | 高性能、低延迟、支持多协议、Docker 部署简单 | -| 后端框架 | Node.js + Express | 轻量级、异步处理能力强、生态丰富 | -| 前端框架 | React 18 | 组件化开发、生态成熟、性能优秀 | -| 视频播放器 | flv.js + hls.js | flv.js 支持 HTTP-FLV 低延迟播放,hls.js 提供 HLS 兼容性 | -| 容器化 | Docker + Docker Compose | 简化部署、环境一致性 | - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 用户层 (Users) │ -├─────────────────────────────────────────────────────────────────┤ -│ ┌─────────────┐ ┌─────────────────┐ │ -│ │ 主播 │ │ 观众 │ │ -│ │ (OBS/FFmpeg)│ │ (浏览器) │ │ -│ └──────┬──────┘ └────────┬────────┘ │ -│ │ RTMP 推流 │ HTTP 请求 │ -│ │ rtmp://host:1935/live/{streamKey} │ │ -└─────────┼─────────────────────────────────────────┼────────────┘ - │ │ -┌─────────┼─────────────────────────────────────────┼────────────┐ -│ ▼ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ SRS Server │ │ React Frontend │ │ -│ │ (Docker) │ │ (Port 3000) │ │ -│ │ │ │ │ │ -│ │ RTMP: 1935 │◄───HTTP-FLV/HLS───│ flv.js/hls.js │ │ -│ │ HTTP: 8080 │ │ │ │ -│ └────────┬────────┘ └────────┬────────┘ │ -│ │ │ │ -│ │ HTTP Callback │ REST API │ -│ │ (on_publish/on_unpublish) │ │ -│ ▼ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Node.js API Server │ │ -│ │ (Port 3001) │ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ -│ │ │ Room API │ │ Callback │ │ Room Store │ │ │ -│ │ │ Controller │ │ Handler │ │ (In-Memory) │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ 服务层 (Services) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 数据流说明 - -1. **推流流程**: 主播 → RTMP → SRS → HTTP Callback → API Server → 更新房间状态 -2. **观看流程**: 观众 → React App → 获取房间信息 → flv.js/hls.js → SRS HTTP-FLV/HLS -3. **管理流程**: 用户 → React App → REST API → API Server → 房间 CRUD - -## Components and Interfaces - -### 1. SRS 流媒体服务器 - -**职责**: 接收 RTMP 推流,转换并分发 HTTP-FLV 和 HLS 流 - -**配置接口**: -```conf -# srs.conf -listen 1935; -max_connections 1000; -daemon off; -srs_log_tank console; - -http_server { - enabled on; - listen 8080; - dir ./objs/nginx/html; -} - -vhost __defaultVhost__ { - hls { - enabled on; - hls_fragment 2; - hls_window 10; - hls_path ./objs/nginx/html; - hls_m3u8_file [app]/[stream].m3u8; - hls_ts_file [app]/[stream]-[seq].ts; - } - - http_remux { - enabled on; - mount [vhost]/[app]/[stream].flv; - } - - http_hooks { - enabled on; - on_publish http://api-server:3001/api/srs/on_publish; - on_unpublish http://api-server:3001/api/srs/on_unpublish; - } -} -``` - -### 2. API Server (Node.js + Express) - -**职责**: 提供业务 API,处理 SRS 回调,管理房间数据 - -#### 2.1 Room API Controller - -```typescript -// 接口定义 -interface RoomController { - // 获取所有房间 - GET /api/rooms -> Room[] - - // 创建房间 - POST /api/rooms { title: string, streamerName: string } -> Room - - // 获取单个房间 - GET /api/rooms/:id -> Room - - // 删除房间 - DELETE /api/rooms/:id -> { success: boolean } -} -``` - -#### 2.2 SRS Callback Handler - -```typescript -// SRS 回调接口 -interface SRSCallbackHandler { - // 推流开始回调 - POST /api/srs/on_publish { - action: 'on_publish', - app: string, // 应用名,如 'live' - stream: string // 流名称,即 streamKey - } -> { code: 0 } - - // 推流结束回调 - POST /api/srs/on_unpublish { - action: 'on_unpublish', - app: string, - stream: string - } -> { code: 0 } -} -``` - -### 3. React Frontend - -**职责**: 提供用户界面,包括房间列表、直播播放、房间创建 - -#### 3.1 组件结构 - -``` -src/ -├── components/ -│ ├── RoomList.jsx # 房间列表组件 -│ ├── RoomCard.jsx # 房间卡片组件 -│ ├── LivePlayer.jsx # 直播播放器组件 -│ ├── CreateRoom.jsx # 创建房间弹窗 -│ └── StreamInfo.jsx # 推流信息展示 -├── pages/ -│ ├── HomePage.jsx # 首页(房间列表) -│ └── RoomPage.jsx # 房间详情页 -├── services/ -│ └── api.js # API 调用封装 -└── App.jsx # 应用入口 -``` - -#### 3.2 LivePlayer 组件接口 - -```typescript -interface LivePlayerProps { - room: Room; - preferFlv?: boolean; // 优先使用 FLV(低延迟) -} - -// 播放器内部逻辑 -// 1. 检测浏览器是否支持 flv.js (需要 MSE 支持) -// 2. 支持则使用 HTTP-FLV,否则降级到 HLS -// 3. 监听播放错误,自动重连 -``` - -## Data Models - -### Room 数据模型 - -```typescript -interface Room { - id: string; // 房间唯一标识 (UUID) - title: string; // 房间标题 - streamerName: string; // 主播名称 - streamKey: string; // 推流密钥 (与 id 相同) - isLive: boolean; // 是否正在直播 - viewerCount: number; // 观看人数 - createdAt: string; // 创建时间 (ISO 8601) - startedAt?: string; // 开播时间 (ISO 8601) -} -``` - -### API 响应格式 - -```typescript -// 成功响应 -interface SuccessResponse { - success: true; - data: T; -} - -// 错误响应 -interface ErrorResponse { - success: false; - error: { - code: string; - message: string; - }; -} - -// SRS 回调响应 -interface SRSCallbackResponse { - code: number; // 0 表示成功 -} -``` - -### 流地址格式 - -```typescript -interface StreamUrls { - rtmp: string; // rtmp://host:1935/live/{streamKey} - flv: string; // http://host:8080/live/{streamKey}.flv - hls: string; // http://host:8080/live/{streamKey}.m3u8 -} -``` - - - -## Correctness Properties - -*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* - -### Property 1: Room Creation Round-Trip Consistency - -*For any* valid room creation request with non-empty title and streamer name, creating the room and then retrieving it by ID SHALL return a room with matching title, streamer name, a unique stream key, and a valid creation timestamp. - -**Validates: Requirements 1.1, 1.4** - -### Property 2: New Rooms Start Offline - -*For any* newly created room, the initial `isLive` status SHALL be `false`. - -**Validates: Requirements 1.2** - -### Property 3: Empty Title Rejection - -*For any* room creation request where the title is empty or consists only of whitespace characters, the system SHALL reject the request and return a validation error without creating a room. - -**Validates: Requirements 1.3** - -### Property 4: Publish Callback Status Transition - -*For any* room that exists in the system, when a publish callback is received with that room's stream key, the room's `isLive` status SHALL transition to `true`. When an unpublish callback is received, the status SHALL transition to `false`. - -**Validates: Requirements 2.3, 2.5** - -### Property 5: Stream URLs Contain Required Formats - -*For any* room returned by the API, the stream URLs SHALL include both HTTP-FLV (`.flv`) and HLS (`.m3u8`) format URLs containing the room's stream key. - -**Validates: Requirements 3.1** - -### Property 6: Room List Completeness - -*For any* set of created rooms, requesting the room list SHALL return all rooms with their current status, title, and streamer name intact. - -**Validates: Requirements 4.1, 4.2** - -## Error Handling - -### API Error Codes - -| 错误码 | 描述 | HTTP 状态码 | -|--------|------|-------------| -| `VALIDATION_ERROR` | 请求参数验证失败 | 400 | -| `ROOM_NOT_FOUND` | 房间不存在 | 404 | -| `STREAM_KEY_INVALID` | 推流密钥无效 | 401 | -| `INTERNAL_ERROR` | 服务器内部错误 | 500 | - -### 错误处理策略 - -1. **API 层**: 统一错误响应格式,包含错误码和消息 -2. **SRS 回调**: 返回 `{ code: 0 }` 表示成功,非零表示失败 -3. **前端**: 显示友好的错误提示,支持重试操作 -4. **播放器**: 自动重连机制,最多重试 3 次 - -## Testing Strategy - -### 单元测试 - -使用 Jest 进行单元测试,覆盖以下模块: - -1. **Room Service**: 房间 CRUD 操作 -2. **Validation**: 输入验证逻辑 -3. **URL Generator**: 流地址生成 - -### 属性测试 (Property-Based Testing) - -使用 fast-check 库进行属性测试: - -1. **Property 1**: 房间创建往返一致性 -2. **Property 2**: 新房间初始状态 -3. **Property 3**: 空标题拒绝 -4. **Property 4**: 回调状态转换 -5. **Property 5**: 流地址格式 -6. **Property 6**: 房间列表完整性 - -### 测试配置 - -```javascript -// jest.config.js -module.exports = { - testEnvironment: 'node', - testMatch: ['**/*.test.js', '**/*.property.test.js'], - collectCoverageFrom: ['server/**/*.js'], - coverageThreshold: { - global: { branches: 80, functions: 80, lines: 80 } - } -}; -``` - -### 属性测试示例 - -```javascript -// 每个属性测试运行 100 次迭代 -// 标注格式: **Feature: live-streaming-system, Property {number}: {property_text}** - -import fc from 'fast-check'; - -// **Feature: live-streaming-system, Property 2: New Rooms Start Offline** -test('newly created rooms should have isLive = false', () => { - fc.assert( - fc.property( - fc.string({ minLength: 1 }), // title - fc.string({ minLength: 1 }), // streamerName - (title, streamerName) => { - const room = createRoom({ title, streamerName }); - return room.isLive === false; - } - ), - { numRuns: 100 } - ); -}); -``` \ No newline at end of file diff --git a/.kiro/specs/live-streaming-system/requirements.md b/.kiro/specs/live-streaming-system/requirements.md deleted file mode 100644 index a8f53208..00000000 --- a/.kiro/specs/live-streaming-system/requirements.md +++ /dev/null @@ -1,89 +0,0 @@ -# Requirements Document - -## Introduction - -本文档定义了一个基于 SRS(Simple Realtime Server)的个人直播系统的功能需求。该系统允许用户创建直播间、进行实时视频推流、观看直播内容,并提供基本的直播间管理功能。系统采用 SRS 作为核心流媒体服务器,支持 RTMP 推流和 HTTP-FLV/HLS 播放,具有高性能、低延迟的特点。 - -## Glossary - -- **Live Streaming System(直播系统)**: 提供视频直播功能的完整系统,包含前端播放器、后端 API 服务和 SRS 流媒体服务 -- **SRS(Simple Realtime Server)**: 开源的高性能流媒体服务器,支持 RTMP、HLS、HTTP-FLV、WebRTC 等协议 -- **Streamer(主播)**: 创建直播间并推送视频流的用户 -- **Viewer(观众)**: 观看直播内容的用户 -- **Live Room(直播间)**: 主播进行直播的虚拟空间,包含标题、主播信息和直播状态 -- **Stream Key(推流密钥)**: 用于验证主播身份的唯一标识符 -- **RTMP(Real-Time Messaging Protocol)**: 用于主播推流的实时消息传输协议 -- **HTTP-FLV**: 基于 HTTP 的 FLV 流协议,延迟低于 HLS,适合实时直播 -- **HLS(HTTP Live Streaming)**: 基于 HTTP 的直播流协议,兼容性好但延迟较高 -- **SRS Callback**: SRS 的 HTTP 回调机制,用于通知业务服务器推流状态变化 - -## Requirements - -### Requirement 1 - -**User Story:** As a streamer, I want to create a live room, so that I can start broadcasting my content to viewers. - -#### Acceptance Criteria - -1. WHEN a streamer submits a room creation request with title and streamer name, THE Live Streaming System SHALL create a new live room and return a unique stream key -2. WHEN a live room is created, THE Live Streaming System SHALL initialize the room status as "offline" -3. WHEN a streamer provides an empty title, THE Live Streaming System SHALL reject the creation request and return a validation error -4. WHEN a live room is created, THE Live Streaming System SHALL store the room information including title, streamer name, stream key, and creation timestamp - -### Requirement 2 - -**User Story:** As a streamer, I want to push my video stream to the server, so that viewers can watch my live content. - -#### Acceptance Criteria - -1. WHEN a streamer connects to the SRS RTMP endpoint with a valid stream key, THE SRS Server SHALL accept the connection and begin receiving the video stream -2. WHEN SRS receives a publish event, THE SRS Server SHALL trigger an HTTP callback to notify the API service of the stream start -3. WHEN the API service receives a publish callback, THE Live Streaming System SHALL update the corresponding room status to "live" -4. WHEN SRS receives an unpublish event, THE SRS Server SHALL trigger an HTTP callback to notify the API service of the stream end -5. WHEN the API service receives an unpublish callback, THE Live Streaming System SHALL update the room status to "offline" - -### Requirement 3 - -**User Story:** As a viewer, I want to watch live streams, so that I can enjoy the content broadcasted by streamers. - -#### Acceptance Criteria - -1. WHEN a viewer requests to watch a live room, THE Live Streaming System SHALL provide both HTTP-FLV and HLS stream URLs for playback -2. WHEN a viewer accesses an HTTP-FLV stream URL, THE Live Streaming System SHALL deliver the video with latency under 3 seconds -3. WHEN a viewer accesses an HLS stream URL, THE Live Streaming System SHALL deliver the video with latency under 10 seconds -4. WHEN a viewer attempts to watch an offline room, THE Live Streaming System SHALL display an appropriate offline message -5. WHEN the stream quality changes, THE Live Streaming System SHALL continue playback without interruption - -### Requirement 4 - -**User Story:** As a viewer, I want to browse available live rooms, so that I can discover content to watch. - -#### Acceptance Criteria - -1. WHEN a viewer requests the room list, THE Live Streaming System SHALL return all available rooms with their current status -2. WHEN displaying the room list, THE Live Streaming System SHALL show room title, streamer name, and live status for each room -3. WHEN a room's live status changes, THE Live Streaming System SHALL reflect the updated status within 5 seconds on subsequent requests -4. WHEN no rooms exist, THE Live Streaming System SHALL return an empty list with appropriate messaging - -### Requirement 5 - -**User Story:** As a system administrator, I want to deploy and configure SRS, so that the streaming service runs reliably. - -#### Acceptance Criteria - -1. WHEN deploying SRS, THE Live Streaming System SHALL use Docker for containerized deployment -2. WHEN configuring SRS, THE Live Streaming System SHALL enable RTMP input on port 1935 -3. WHEN configuring SRS, THE Live Streaming System SHALL enable HTTP-FLV output on port 8080 -4. WHEN configuring SRS, THE Live Streaming System SHALL enable HLS output with 2-second segments -5. WHEN configuring SRS, THE Live Streaming System SHALL set up HTTP callbacks for publish and unpublish events - -### Requirement 6 - -**User Story:** As a user, I want a responsive web interface, so that I can access the live streaming platform from any device. - -#### Acceptance Criteria - -1. WHEN a user accesses the platform, THE Live Streaming System SHALL display a responsive interface that adapts to screen sizes from 320px to 1920px width -2. WHEN a user interacts with the video player, THE Live Streaming System SHALL provide play, pause, and volume controls -3. WHEN a user navigates between pages, THE Live Streaming System SHALL maintain smooth transitions without full page reloads -4. WHEN the interface loads, THE Live Streaming System SHALL display the main content within 3 seconds on a standard broadband connection diff --git a/.kiro/specs/live-streaming-system/tasks.md b/.kiro/specs/live-streaming-system/tasks.md deleted file mode 100644 index 51926c48..00000000 --- a/.kiro/specs/live-streaming-system/tasks.md +++ /dev/null @@ -1,66 +0,0 @@ -# Implementation Tasks - -## Task 1: 项目初始化和 Docker 配置 - -- [ ] 1.1 创建项目根目录结构 (server/, client/, docker/) -- [ ] 1.2 创建 package.json 配置后端依赖 -- [ ] 1.3 创建 SRS 配置文件 (docker/srs/srs.conf) -- [ ] 1.4 创建 docker-compose.yml 配置 SRS 和 API 服务 -- [ ] 1.5 创建 .env.example 环境变量模板 - -## Task 2: 后端 API 服务 - 房间管理 - -- [ ] 2.1 创建 Express 服务入口 (server/index.js) -- [ ] 2.2 实现房间数据存储模块 (server/store/roomStore.js) -- [ ] 2.3 实现房间 API 路由 (server/routes/rooms.js) - - GET /api/rooms - 获取所有房间 - - POST /api/rooms - 创建房间 - - GET /api/rooms/:id - 获取单个房间 - - DELETE /api/rooms/:id - 删除房间 -- [ ] 2.4 实现输入验证中间件 (server/middleware/validate.js) -- [ ] 2.5 实现统一错误处理 (server/middleware/errorHandler.js) - -## Task 3: 后端 API 服务 - SRS 回调处理 - -- [ ] 3.1 实现 SRS 回调路由 (server/routes/srs.js) - - POST /api/srs/on_publish - 推流开始回调 - - POST /api/srs/on_unpublish - 推流结束回调 -- [ ] 3.2 实现流地址生成工具 (server/utils/streamUrl.js) -- [ ] 3.3 添加回调日志记录 - -## Task 4: 前端 - 项目搭建和基础组件 - -- [ ] 4.1 初始化 React 项目 (client/) -- [ ] 4.2 安装依赖 (flv.js, hls.js, axios) -- [ ] 4.3 创建 API 服务封装 (client/src/services/api.js) -- [ ] 4.4 创建全局样式 (client/src/index.css) -- [ ] 4.5 创建 App 入口和路由 (client/src/App.jsx) - -## Task 5: 前端 - 房间列表页面 - -- [ ] 5.1 创建 RoomCard 组件 (client/src/components/RoomCard.jsx) -- [ ] 5.2 创建 RoomList 组件 (client/src/components/RoomList.jsx) -- [ ] 5.3 创建 HomePage 页面 (client/src/pages/HomePage.jsx) -- [ ] 5.4 实现房间列表自动刷新 - -## Task 6: 前端 - 创建直播间功能 - -- [ ] 6.1 创建 CreateRoom 弹窗组件 (client/src/components/CreateRoom.jsx) -- [ ] 6.2 创建 StreamInfo 推流信息组件 (client/src/components/StreamInfo.jsx) -- [ ] 6.3 实现复制推流地址功能 - -## Task 7: 前端 - 直播播放器 - -- [ ] 7.1 创建 LivePlayer 组件 (client/src/components/LivePlayer.jsx) -- [ ] 7.2 实现 flv.js 播放器集成 (HTTP-FLV 低延迟) -- [ ] 7.3 实现 hls.js 降级播放 (HLS 兼容性) -- [ ] 7.4 实现播放器自动重连机制 -- [ ] 7.5 创建 RoomPage 直播间页面 (client/src/pages/RoomPage.jsx) - -## Task 8: 集成测试和部署 - -- [ ] 8.1 编写属性测试 (server/tests/room.property.test.js) -- [ ] 8.2 编写 API 集成测试 (server/tests/api.test.js) -- [ ] 8.3 创建启动脚本 (scripts/start.sh, scripts/start.bat) -- [ ] 8.4 编写 README.md 使用文档 -- [ ] 8.5 测试完整推流和观看流程 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java index c96d936a..3dea540a 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java @@ -9,7 +9,6 @@ import com.zbkj.front.response.live.StreamUrlsResponse; import com.zbkj.service.service.LiveRoomService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.validation.annotation.Validated; @@ -79,7 +78,10 @@ public class LiveRoomController { private LiveRoomResponse toResponse(LiveRoom room, String requestHost) { LiveRoomResponse resp = new LiveRoomResponse(); - BeanUtils.copyProperties(room, resp); + resp.setId(room.getId() == null ? null : String.valueOf(room.getId())); + resp.setTitle(room.getTitle()); + resp.setStreamerName(room.getStreamerName()); + resp.setStreamKey(room.getStreamKey()); resp.setIsLive(room.getIsLive() != null && room.getIsLive() == 1); resp.setViewerCount(0); diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java index c27e1c2f..6ffa5c93 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveRoomResponse.java @@ -9,7 +9,7 @@ import lombok.Data; public class LiveRoomResponse { @ApiModelProperty(value = "房间ID") - private Integer id; + private String id; @ApiModelProperty(value = "标题") private String title; diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index abd573d8..58a1001e 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -32,10 +32,10 @@ android { } val apiBaseUrlEmulator = (localProps.getProperty("api.base_url_emulator") - ?: "http://10.0.2.2:25001/api/").trim() + ?: "http://10.0.2.2:8081/").trim() val apiBaseUrlDevice = (localProps.getProperty("api.base_url_device") - ?: "http://192.168.1.100:25001/api/").trim() + ?: "http://192.168.1.100:8081/").trim() // 模拟器使用服务器地址 buildConfigField("String", "API_BASE_URL_EMULATOR", "\"$apiBaseUrlEmulator\"") diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 3e39b0a7..ca186135 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -62,6 +62,8 @@ public class MainActivity extends AppCompatActivity { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + ApiClient.getService(getApplicationContext()); + // 立即显示缓存数据,提升启动速度 setupRecyclerView(); setupUI(); @@ -417,7 +419,7 @@ public class MainActivity extends AppCompatActivity { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - ApiClient.getService().createRoom(new CreateRoomRequest(title, streamer)) + ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamer)) .enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -425,8 +427,9 @@ public class MainActivity extends AppCompatActivity { ApiResponse body = response.body(); Room room = body != null ? body.getData() : null; - if (!response.isSuccessful() || room == null) { - Toast.makeText(MainActivity.this, "创建失败", Toast.LENGTH_SHORT).show(); + if (!response.isSuccessful() || body == null || !body.isOk() || room == null) { + String msg = body != null && !TextUtils.isEmpty(body.getMessage()) ? body.getMessage() : "创建失败"; + Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show(); return; } @@ -535,14 +538,16 @@ public class MainActivity extends AppCompatActivity { binding.loading.setVisibility(View.VISIBLE); } - ApiClient.getService().getRooms().enqueue(new Callback>>() { + ApiClient.getService(getApplicationContext()).getRooms().enqueue(new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { binding.loading.setVisibility(View.GONE); binding.swipeRefresh.setRefreshing(false); isFetching = false; ApiResponse> body = response.body(); - List rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList(); + List rooms = response.isSuccessful() && body != null && body.isOk() && body.getData() != null + ? body.getData() + : Collections.emptyList(); if (rooms == null || rooms.isEmpty()) { rooms = buildDemoRooms(12); } diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index 07d43522..60c86949 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -91,6 +91,8 @@ public class RoomDetailActivity extends AppCompatActivity { binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + ApiClient.getService(getApplicationContext()); + roomId = getIntent().getStringExtra(EXTRA_ROOM_ID); triedAltUrl = false; @@ -310,19 +312,21 @@ public class RoomDetailActivity extends AppCompatActivity { binding.loading.setVisibility(View.VISIBLE); } - ApiClient.getService().getRoom(roomId).enqueue(new Callback>() { + ApiClient.getService(getApplicationContext()).getRoom(roomId).enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (isFinishing() || isDestroyed()) return; binding.loading.setVisibility(View.GONE); + boolean firstLoad = isFirstLoad; isFirstLoad = false; ApiResponse body = response.body(); Room data = body != null ? body.getData() : null; - if (!response.isSuccessful() || data == null) { - if (isFirstLoad) { - Toast.makeText(RoomDetailActivity.this, "房间不存在", Toast.LENGTH_SHORT).show(); + if (!response.isSuccessful() || body == null || !body.isOk() || data == null) { + if (firstLoad) { + String msg = body != null && !TextUtils.isEmpty(body.getMessage()) ? body.getMessage() : "房间不存在"; + Toast.makeText(RoomDetailActivity.this, msg, Toast.LENGTH_SHORT).show(); finish(); } return; diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java index b0ded619..978ef8d0 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java @@ -3,9 +3,14 @@ package com.example.livestreaming.net; import android.os.Build; import android.util.Log; +import android.content.Context; + import com.example.livestreaming.BuildConfig; +import androidx.annotation.Nullable; + import okhttp3.OkHttpClient; +import okhttp3.Interceptor; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @@ -15,6 +20,7 @@ public final class ApiClient { private static final String TAG = "ApiClient"; private static volatile Retrofit retrofit; private static volatile ApiService service; + private static volatile Context appContext; private ApiClient() { } @@ -47,6 +53,13 @@ public final class ApiClient { } public static ApiService getService() { + return getService(null); + } + + public static ApiService getService(@Nullable Context context) { + if (context != null) { + appContext = context.getApplicationContext(); + } if (service != null) return service; synchronized (ApiClient.class) { if (service != null) return service; @@ -54,8 +67,19 @@ public final class ApiClient { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + Interceptor auth = chain -> { + Context ctx = ApiClient.appContext; + String token = ctx != null ? AuthStore.getToken(ctx) : null; + okhttp3.Request.Builder req = chain.request().newBuilder(); + if (token != null && !token.trim().isEmpty()) { + req.header("Authori-zation", token.trim()); + } + return chain.proceed(req.build()); + }; + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .addInterceptor(logging) + .addInterceptor(auth) .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS) .writeTimeout(15, java.util.concurrent.TimeUnit.SECONDS) diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiResponse.java index eb7ce882..65849c86 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiResponse.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiResponse.java @@ -4,17 +4,28 @@ import com.google.gson.annotations.SerializedName; public class ApiResponse { - @SerializedName("success") - private Boolean success; + @SerializedName("code") + private Long code; + + @SerializedName("message") + private String message; @SerializedName("data") private T data; - public Boolean getSuccess() { - return success; + public Long getCode() { + return code; + } + + public String getMessage() { + return message; } public T getData() { return data; } + + public boolean isOk() { + return code != null && code == 200L; + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java index e18816d0..d447ea44 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java @@ -10,15 +10,18 @@ import retrofit2.http.Path; import retrofit2.http.POST; public interface ApiService { - @GET("rooms") + @POST("api/front/login") + Call> login(@Body LoginRequest body); + + @GET("api/front/live/public/rooms") Call>> getRooms(); - @POST("rooms") + @POST("api/front/live/rooms") Call> createRoom(@Body CreateRoomRequest body); - @GET("rooms/{id}") + @GET("api/front/live/public/rooms/{id}") Call> getRoom(@Path("id") String id); - @DELETE("rooms/{id}") + @DELETE("api/front/live/rooms/{id}") Call> deleteRoom(@Path("id") String id); } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java b/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java new file mode 100644 index 00000000..1ce09365 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java @@ -0,0 +1,36 @@ +package com.example.livestreaming.net; + +import android.content.Context; + +import androidx.annotation.Nullable; + +public final class AuthStore { + + private static final String PREFS = "auth_prefs"; + private static final String KEY_TOKEN = "token"; + + private AuthStore() { + } + + public static void setToken(Context context, @Nullable String token) { + if (context == null) return; + if (token == null || token.trim().isEmpty()) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .remove(KEY_TOKEN) + .apply(); + return; + } + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putString(KEY_TOKEN, token.trim()) + .apply(); + } + + @Nullable + public static String getToken(Context context) { + if (context == null) return null; + return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_TOKEN, null); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/LoginRequest.java b/android-app/app/src/main/java/com/example/livestreaming/net/LoginRequest.java new file mode 100644 index 00000000..f9ac7106 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/net/LoginRequest.java @@ -0,0 +1,25 @@ +package com.example.livestreaming.net; + +import com.google.gson.annotations.SerializedName; + +public class LoginRequest { + + @SerializedName("account") + private final String account; + + @SerializedName("password") + private final String password; + + public LoginRequest(String account, String password) { + this.account = account; + this.password = password; + } + + public String getAccount() { + return account; + } + + public String getPassword() { + return password; + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java new file mode 100644 index 00000000..ea835cf5 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java @@ -0,0 +1,13 @@ +package com.example.livestreaming.net; + +import com.google.gson.annotations.SerializedName; + +public class LoginResponse { + + @SerializedName("token") + private String token; + + public String getToken() { + return token; + } +}