跳转到内容

播放/停止命令

播放/停止命令

本节详细介绍音频播放控制系统中的播放、暂停和停止命令的实现。学习完成后,您将能够:

  • 实现播放器的状态管理(播放中、暂停、停止)
  • 处理 MQTT 远程控制命令
  • 理解音频流播放状态的切换逻辑
  • 实现暂停/恢复功能

在开始本节之前,请确保:

  • 已完成 MQTT 远程控制配置
  • 音频播放功能正常工作
  • 了解基本的有限状态机概念
┌───────────────────┐
│ STOPPED │
└────────┬──────────┘
│ play
┌───────────────────┐
┌───│ PLAYING │◄────┐
│ └────────┬──────────┘ │
│ │ │
│ pause │ resume │
│ ▼ │
│ ┌───────────────────┐ │
└───│ PAUSED ├─────┘
└───────────────────┘
stop/end
┌───────────────────┐
│ STOPPED │
└───────────────────┘
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 播放控制回调
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);
}
}

对于更复杂的控制需求,可以使用 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();
}
}

音频流可能因为网络问题或电台广播结束而断开,需要自动重连:

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 中创建控制流程:

{
"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"
}
}
}

症状: 调用 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() 会从流当前位置继续(对于直播流影响不大,对于文件会丢失位置)

解决方案: 对于网络电台直播流,暂停后恢复会自动继续当前位置(因为服务器持续发送新数据)。

本节介绍了播放/停止命令的完整实现:

  1. 状态管理:播放器的三种状态(播放中、暂停、停止)
  2. 播放控制:通过 MQTT 命令实现播放、暂停、恢复、停止
  3. JSON 控制:使用 JSON 格式传递复杂控制命令
  4. 自动重连:流断开后的自动重连机制
  5. Node-RED 集成:通过仪表板按钮控制播放器