播放/停止命令
播放/停止命令
本节详细介绍音频播放控制系统中的播放、暂停和停止命令的实现。学习完成后,您将能够:
- 实现播放器的状态管理(播放中、暂停、停止)
- 处理 MQTT 远程控制命令
- 理解音频流播放状态的切换逻辑
- 实现暂停/恢复功能
在开始本节之前,请确保:
- 已完成 MQTT 远程控制配置
- 音频播放功能正常工作
- 了解基本的有限状态机概念
Player State Management
Section titled “Player State Management”Player States
Section titled “Player States” ┌───────────────────┐ │ STOPPED │ └────────┬──────────┘ │ play ▼ ┌───────────────────┐ ┌───│ PLAYING │◄────┐ │ └────────┬──────────┘ │ │ │ │ │ pause │ resume │ │ ▼ │ │ ┌───────────────────┐ │ └───│ PAUSED ├─────┘ └───────────────────┘ │ stop/end ▼ ┌───────────────────┐ │ STOPPED │ └───────────────────┘State Implementation
Section titled “State Implementation”enum PlayerState { STATE_STOPPED, STATE_PLAYING, STATE_PAUSED};
PlayerState currentState = STATE_STOPPED;
// 播放状态管理class PlayerController {private: PlayerState state; AudioGenerator* decoder; AudioFileSource* source; AudioOutput* output; AudioFileSource* pauseSource; // 暂停时保存的文件源引用
public: PlayerController() : state(STATE_STOPPED), decoder(nullptr), source(nullptr), output(nullptr), pauseSource(nullptr) {}
// 设置核心对象引用 void setDecoder(AudioGenerator* d) { decoder = d; } void setSource(AudioFileSource* s) { source = s; } void setOutput(AudioOutput* o) { output = o; }
// 开始播放 bool play(const char* url) { if (!source || !decoder || !output) return false;
// 如果正在播放,先停止 if (state == STATE_PLAYING || state == STATE_PAUSED) { stop(); }
// 打开新音频流 source->open(url); decoder->begin(source, output); state = STATE_PLAYING;
Serial.println("▶️ 开始播放"); return true; }
// 暂停播放 bool pause() { if (state != STATE_PLAYING) return false;
if (decoder->isRunning()) { decoder->stop(); } state = STATE_PAUSED; Serial.println("⏸️ 已暂停"); return true; }
// 恢复播放 bool resume() { if (state != STATE_PAUSED) return false;
decoder->begin(source, output); state = STATE_PLAYING; Serial.println("▶️ 恢复播放"); return true; }
// 停止播放 bool stop() { if (state == STATE_STOPPED) return false;
if (decoder->isRunning()) { decoder->stop(); } source->close(); state = STATE_STOPPED; Serial.println("⏹️ 已停止"); return true; }
// 获取状态 PlayerState getState() { return state; } bool isPlaying() { return state == STATE_PLAYING; } bool isPaused() { return state == STATE_PAUSED; }};
PlayerController player;MQTT Command Integration
Section titled “MQTT Command Integration”Play/Stop MQTT Handler
Section titled “Play/Stop MQTT Handler”// MQTT 播放控制回调void playControlHandler(String message) { if (message == "play" || message == "1") { if (player.getState() == STATE_PAUSED) { player.resume(); } else { player.play(stationURLs[currentStation]); } } else if (message == "stop" || message == "0") { player.stop(); } else if (message == "pause") { player.pause(); } else if (message == "resume") { player.resume(); } else if (message == "toggle") { if (player.isPlaying()) { player.pause(); } else { player.resume(); } }}
// 在 mqttCallback 中调用void mqttCallback(char* topic, byte* payload, unsigned int length) { String message; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; }
if (strcmp(topic, "factory/broadcast/control/play") == 0) { playControlHandler(message); }}Playback Control via JSON
Section titled “Playback Control via JSON”JSON Control Format
Section titled “JSON Control Format”对于更复杂的控制需求,可以使用 JSON 格式传递多个参数:
// JSON 控制格式// {// "cmd": "play",// "station": 2,// "volume": 80,// "duration": 3600// }
void handleJSONCommand(const char* jsonString) { StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, jsonString);
if (error) { Serial.printf("JSON 解析失败: %s\n", error.c_str()); return; }
const char* command = doc["cmd"];
if (strcmp(command, "play") == 0) { int station = doc["station"] | currentStation; switchStation(station);
if (doc.containsKey("volume")) { setVolume(doc["volume"]); }
// 定时停止(单位:秒) if (doc.containsKey("duration")) { int duration = doc["duration"]; // 实现定时停止逻辑 } } else if (strcmp(command, "stop") == 0) { player.stop(); } else if (strcmp(command, "pause") == 0) { player.pause(); } else if (strcmp(command, "resume") == 0) { player.resume(); }}Auto-Reconnect and Loop
Section titled “Auto-Reconnect and Loop”Stream Reconnection Logic
Section titled “Stream Reconnection Logic”音频流可能因为网络问题或电台广播结束而断开,需要自动重连:
void checkAndReconnect() { // 如果播放器正在播放,但解码器报告结束 if (player.isPlaying() && !decoder->isRunning()) { Serial.println("音频流已断开,尝试重连...");
// 上报状态 mqttClient.publish("factory/broadcast/status", "{\"event\":\"stream_disconnected\"}");
// 等待一段时间后重试 delay(3000);
// 尝试重新连接 int retries = 3; while (retries > 0) { file->close(); delay(500); file->open(stationURLs[currentStation]);
if (file->isOpen()) { mp3->begin(file, output); Serial.println("重连成功"); return; }
retries--; delay(2000); }
Serial.println("重连失败,等待下次尝试"); }}Node-RED Control Integration
Section titled “Node-RED Control Integration”Node-RED Play Control Flow
Section titled “Node-RED Play Control Flow”在 Node-RED 中创建控制流程:
{ "inject": {"topic": "factory/broadcast/control/play", "payload": "play"}, "inject": {"topic": "factory/broadcast/control/play", "payload": "stop"}, "inject": {"topic": "factory/broadcast/control/play", "payload": "pause"},
"mqtt out": { "server": "localhost", "topic": "factory/broadcast/control/play", "qos": 1 }}Node-RED 仪表板控制:
// 按钮节点配置{ "dashboard": { "group": "广播控制", "button": { "label": "开始/停止", "msg": "toggle", "topic": "factory/broadcast/control/play" } }}问题 1: 停止后无法重新播放
Section titled “问题 1: 停止后无法重新播放”症状: 调用 play() 后没有任何声音,且 decoder->isRunning() 返回 false
原因:
- 未正确关闭之前的流连接
- 文件源对象处于错误状态
解决方案:
// 停止时释放所有资源void safeStop() { if (decoder && decoder->isRunning()) { decoder->stop(); delay(100); // 等待解码器完全停止 } if (source) { source->close(); delay(50); // 等待连接关闭 } delay(200); // 确保资源释放}问题 2: 暂停后恢复播放位置丢失
Section titled “问题 2: 暂停后恢复播放位置丢失”症状: 恢复播放时从头开始,而不是从暂停位置继续
原因: ESP8266Audio 库的某些版本不支持真正的暂停/恢复,stop() 后重新 begin() 会从流当前位置继续(对于直播流影响不大,对于文件会丢失位置)
解决方案: 对于网络电台直播流,暂停后恢复会自动继续当前位置(因为服务器持续发送新数据)。
Summary
Section titled “Summary”本节介绍了播放/停止命令的完整实现:
- 状态管理:播放器的三种状态(播放中、暂停、停止)
- 播放控制:通过 MQTT 命令实现播放、暂停、恢复、停止
- JSON 控制:使用 JSON 格式传递复杂控制命令
- 自动重连:流断开后的自动重连机制
- Node-RED 集成:通过仪表板按钮控制播放器