diff --git a/api.go b/api.go index 1276418..6e08200 100644 --- a/api.go +++ b/api.go @@ -181,6 +181,9 @@ func (s *Server) StreamInfo(ctx context.Context, req *pb.StreamSnapRequest) (res func (s *Server) TaskTree(context.Context, *emptypb.Empty) (res *pb.TaskTreeResponse, err error) { var fillData func(m task.ITask) *pb.TaskTreeData fillData = func(m task.ITask) (res *pb.TaskTreeData) { + if m == nil { + return + } t := m.GetTask() res = &pb.TaskTreeData{ Id: m.GetTaskID(), @@ -197,7 +200,11 @@ func (s *Server) TaskTree(context.Context, *emptypb.Empty) (res *pb.TaskTreeResp res.Blocked = fillData(blockedTask) } for t := range job.RangeSubTask { - res.Children = append(res.Children, fillData(t)) + child := fillData(t) + if child == nil { + continue + } + res.Children = append(res.Children, child) } } return diff --git a/doc_CN/stream_alias_tech.md b/doc_CN/stream_alias_tech.md new file mode 100644 index 0000000..0f0857e --- /dev/null +++ b/doc_CN/stream_alias_tech.md @@ -0,0 +1,158 @@ +# Monibuca 流别名功能技术实现文档 + +## 1. 功能概述 + +流别名(Stream Alias)是 Monibuca 中的一个重要功能,它允许为已存在的流创建一个或多个别名,使得同一个流可以通过不同的路径被访问。这个功能在以下场景特别有用: + +- 为长路径的流创建简短别名 +- 动态修改流的访问路径 +- 实现流的重定向功能 + +## 2. 核心数据结构 + +### 2.1 AliasStream 结构 + +```go +type AliasStream struct { + *Publisher // 继承自 Publisher + AutoRemove bool // 是否自动移除 + StreamPath string // 原始流路径 + Alias string // 别名路径 +} +``` + +### 2.2 StreamAlias 消息结构 + +```protobuf +message StreamAlias { + string streamPath = 1; // 原始流路径 + string alias = 2; // 别名 + bool autoRemove = 3; // 是否自动移除 + uint32 status = 4; // 状态 +} +``` + +## 3. 核心功能实现 + +### 3.1 别名创建和修改 + +当调用 `SetStreamAlias` API 创建或修改别名时,系统会: + +1. 验证并解析目标流路径 +2. 检查目标流是否存在 +3. 处理以下场景: + - 修改现有别名:更新自动移除标志和流路径 + - 创建新别名:初始化新的 AliasStream 结构 +4. 处理订阅者转移或唤醒等待的订阅者 + +### 3.2 Publisher 启动时的别名处理 + +当一个 Publisher 启动时,系统会: + +1. 检查是否存在指向该 Publisher 的别名 +2. 对于每个匹配的别名: + - 如果别名的 Publisher 为空,设置为新的 Publisher + - 如果别名已有 Publisher,转移订阅者到新的 Publisher +3. 唤醒所有等待该流的订阅者 + +### 3.3 Publisher 销毁时的别名处理 + +Publisher 销毁时的处理流程: + +1. 检查是否因被踢出而停止 +2. 从 Streams 中移除 Publisher +3. 遍历所有别名,对于指向该 Publisher 的别名: + - 如果设置了自动移除,则删除该别名 + - 否则保留别名结构 +4. 处理相关订阅者 + +### 3.4 订阅者处理机制 + +当新的订阅请求到来时: + +1. 检查是否存在匹配的别名 +2. 如果存在别名: + - 别名对应的 Publisher 存在:添加订阅者 + - Publisher 不存在:触发 OnSubscribe 事件 +3. 如果不存在别名: + - 检查是否有匹配的正则表达式别名 + - 检查原始流是否存在 + - 根据情况添加订阅者或加入等待列表 + +## 4. API 接口 + +### 4.1 设置别名 + +```http +POST /api/stream/alias +``` + +请求体: +```json +{ + "streamPath": "原始流路径", + "alias": "别名路径", + "autoRemove": false +} +``` + +### 4.2 获取别名列表 + +```http +GET /api/stream/alias +``` + +响应体: +```json +{ + "code": 0, + "message": "", + "data": [ + { + "streamPath": "原始流路径", + "alias": "别名路径", + "autoRemove": false, + "status": 1 + } + ] +} +``` + +## 5. 状态说明 + +别名状态(status)说明: +- 0:初始状态 +- 1:别名已关联 Publisher +- 2:存在同名的原始流 + +## 6. 最佳实践 + +1. 使用自动移除(autoRemove) + - 当需要临时重定向流时,建议启用自动移除 + - 这样在原始流结束时,别名会自动清理 + +2. 别名命名建议 + - 使用简短且有意义的别名 + - 避免使用特殊字符 + - 建议使用规范的路径格式 + +3. 性能考虑 + - 别名机制采用高效的内存映射 + - 订阅者转移时保持连接状态 + - 支持动态修改,无需重启服务 + +## 7. 注意事项 + +1. 别名冲突处理 + - 当创建的别名与现有流路径冲突时,系统会进行适当处理 + - 建议在创建别名前检查是否存在冲突 + +2. 订阅者行为 + - 别名修改时,现有订阅者会被转移到新的流 + - 确保客户端能够处理流重定向 + +3. 资源管理 + - 及时清理不需要的别名 + - 合理使用自动移除功能 + - 监控别名状态,避免资源泄露 +``` \ No newline at end of file diff --git a/doc_CN/stream_alias_usage.md b/doc_CN/stream_alias_usage.md new file mode 100644 index 0000000..285a9f1 --- /dev/null +++ b/doc_CN/stream_alias_usage.md @@ -0,0 +1,206 @@ +# Monibuca 流别名功能使用指南 + +## 1. 功能简介 + +流别名是 Monibuca 提供的一个强大功能,它允许您为同一个流创建多个不同的访问路径。这个功能不仅可以简化流的访问方式,更重要的是能够实现无缝的流内容切换,特别适合直播过程中插入广告等场景。 + +## 2. 基本使用方法 + +### 2.1 创建别名 + +通过 HTTP API 创建别名: + +```bash +curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "live/original", + "alias": "live/simple", + "autoRemove": false + }' +``` + +### 2.2 查看当前别名列表 + +```bash +curl http://localhost:8080/api/stream/alias +``` + +### 2.3 删除别名 + +```bash +curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "alias": "live/simple" + }' +``` + +## 3. 实战案例:直播广告插入 + +### 3.1 场景描述 + +在直播过程中,经常需要在适当的时机插入广告。使用流别名功能,我们可以实现: +- 无缝切换between直播内容和广告 +- 保持观众的持续观看体验 +- 灵活控制广告的插入时机 +- 支持多个广告源的轮换播放 + +### 3.2 实现步骤 + +1. **准备工作** + ```bash + # 假设主直播流的路径为:live/main + # 广告流的路径为:ads/ad1 + ``` + +2. **创建主直播的别名** + ```bash + curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "live/main", + "alias": "live/show", + "autoRemove": false + }' + ``` + +3. **需要插入广告时** + ```bash + # 将别名指向广告流 + curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "ads/ad1", + "alias": "live/show", + "autoRemove": false + }' + ``` + +4. **广告播放结束后** + ```bash + # 将别名重新指向主直播流 + curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "live/main", + "alias": "live/show", + "autoRemove": false + }' + ``` + +### 3.3 效果说明 + +1. **对观众端的影响** + - 观众始终访问 `live/show` 这个固定地址 + - 切换过程对观众无感知 + - 不会出现黑屏或卡顿 + - 无需刷新播放器 + +2. **对直播系统的影响** + - 主播端推流不受影响 + - 支持多路广告源预加载 + - 可以实现精确的时间控制 + - 系统资源占用小 + +## 4. 进阶使用技巧 + +### 4.1 广告轮播方案 + +```bash +# 创建多个广告流的别名 +curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "ads/ad1", + "alias": "ads/current", + "autoRemove": true + }' + +# 通过脚本定时切换不同的广告 +for ad in ad1 ad2 ad3; do + curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d "{ + \"streamPath\": \"ads/$ad\", + \"alias\": \"ads/current\", + \"autoRemove\": true + }" + sleep 30 # 每个广告播放30秒 +done +``` + +### 4.2 使用自动移除功能 + +当广告流结束时自动切回主流: + +```bash +curl -X POST http://localhost:8080/api/stream/alias \ + -H "Content-Type: application/json" \ + -d '{ + "streamPath": "ads/ad1", + "alias": "live/show", + "autoRemove": true + }' +``` + +### 4.3 条件触发广告 + +结合 Monibuca 的其他功能,可以实现: +- 观众数量达到阈值时插入广告 +- 直播时长达到特定值时插入广告 +- 根据直播内容标签触发相关广告 + +## 5. 最佳实践建议 + +1. **广告内容预加载** + - 提前准备好广告流 + - 确保广告源的稳定性 + - 使用缓存机制提高切换速度 + +2. **合理的切换策略** + - 避免频繁切换影响用户体验 + - 选择适当的切换时机 + - 保持广告时长的合理控制 + +3. **监控和统计** + - 记录广告播放情况 + - 监控切换过程是否平滑 + - 统计观众观看数据 + +4. **容错处理** + - 广告流异常时快速切回主流 + - 设置合理的超时时间 + - 做好日志记录 + +## 6. 常见问题解答 + +1. **Q: 切换时观众会感知到卡顿吗?** + A: 不会。流别名的切换是服务器端的操作,对客户端播放器完全透明。 + +2. **Q: 如何确保广告按预期时间播放?** + A: 可以通过脚本控制切换时间,并配合自动移除功能来确保准确性。 + +3. **Q: 支持多少个并发的别名?** + A: 理论上没有限制,但建议根据服务器资源合理使用。 + +4. **Q: 如何处理广告流异常的情况?** + A: 建议使用自动移除功能,并配合监控系统及时发现和处理异常。 + +## 7. 注意事项 + +1. **资源管理** + - 及时清理不再使用的别名 + - 避免创建过多无用的别名 + - 定期检查别名状态 + +2. **性能考虑** + - 控制并发别名数量 + - 合理设置缓存策略 + - 监控服务器资源使用情况 + +3. **用户体验** + - 控制广告频率和时长 + - 确保切换的流畅性 + - 考虑不同网络环境的用户 +``` \ No newline at end of file diff --git a/server.go b/server.go index 4a5390f..c83554a 100644 --- a/server.go +++ b/server.go @@ -145,6 +145,9 @@ func exit() { } var zipReader *zip.ReadCloser +var adminZipLastModTime time.Time +var lastCheckTime time.Time +var checkInterval = time.Second * 3 // 检查间隔为3秒 func init() { Servers.Init() @@ -152,7 +155,18 @@ func init() { time.AfterFunc(3*time.Second, exit) }) Servers.OnDispose(exit) - zipReader, _ = zip.OpenReader("admin.zip") + loadAdminZip() +} + +func loadAdminZip() { + if zipReader != nil { + zipReader.Close() + zipReader = nil + } + if info, err := os.Stat("admin.zip"); err == nil { + adminZipLastModTime = info.ModTime() + zipReader, _ = zip.OpenReader("admin.zip") + } } func (s *Server) GetKey() uint32 { @@ -408,6 +422,16 @@ func (s *Server) OnSubscribe(streamPath string, args url.Values) { } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 检查 admin.zip 是否需要重新加载 + now := time.Now() + if now.Sub(lastCheckTime) > checkInterval { + if info, err := os.Stat("admin.zip"); err == nil && info.ModTime() != adminZipLastModTime { + s.Info("admin.zip changed, reloading...") + loadAdminZip() + } + lastCheckTime = now + } + if zipReader != nil { http.ServeFileFS(w, r, zipReader, strings.TrimPrefix(r.URL.Path, "/admin")) return