package handler import ( "context" "log" "time" "github.com/qy/hw-ws-service/internal/audio" "github.com/qy/hw-ws-service/internal/connection" "github.com/qy/hw-ws-service/internal/rtcclient" ) // HandleStory 处理硬件发来的 {"type":"story"} 指令。 // 在独立 goroutine 中调用,不阻塞消息读取循环。 func HandleStory(conn *connection.Connection, client *rtcclient.Client) { tag := "[story][" + conn.DeviceID + "]" // 整个故事播放流程最长允许 10 分钟 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() // 1. 通知硬件:TTS 开始 if err := conn.SendJSON(map[string]string{"type": "tts", "state": "start"}); err != nil { log.Printf("%s send start failed: %v", tag, err) return } // 确保异常退出时也发送 stop,避免硬件卡住 defer func() { conn.StopPlayback() conn.SendJSON(map[string]string{"type": "tts", "state": "stop"}) //nolint:errcheck }() // 2. 调用 RTC 后端获取故事 story, err := client.FetchStoryByMAC(ctx, conn.DeviceID) if err != nil { log.Printf("%s fetch story error: %v", tag, err) return } if story == nil { log.Printf("%s no story available", tag) return } log.Printf("%s playing: %s", tag, story.Title) // 3. 获取 Opus 帧:优先使用预转码数据,否则实时 ffmpeg 转码 var frames [][]byte if story.OpusURL != "" { frames, err = audio.FetchOpusFrames(ctx, story.OpusURL) if err != nil { log.Printf("%s fetch pre-converted opus failed, fallback to ffmpeg: %v", tag, err) frames = nil // 确保 fallback } else { log.Printf("%s loaded %d pre-converted frames (~%.1fs)", tag, len(frames), float64(len(frames)*audio.FrameDurationMs)/1000) } } if frames == nil { frames, err = audio.MP3URLToOpusFrames(ctx, story.AudioURL) if err != nil { log.Printf("%s audio convert error: %v", tag, err) return } log.Printf("%s converted %d frames (~%.1fs)", tag, len(frames), float64(len(frames)*audio.FrameDurationMs)/1000) } // 4. 通知硬件:句子开始(发送故事标题) if err := conn.SendJSON(map[string]any{ "type": "tts", "state": "sentence_start", "text": story.Title, }); err != nil { log.Printf("%s send sentence_start failed: %v", tag, err) return } // 5. 开始播放,获取打断 channel abortCh := conn.StartPlayback() // 6. 流控推送 Opus 帧 SendOpusStream(conn, frames, abortCh) log.Printf("%s playback finished", tag) // defer 会发送 stop 并调用 StopPlayback }