You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Discord JS Player流媒体状态异常及播放控制失效问题求助

Discord音乐机器人播放器状态异常问题

问题现象

  • 切换到play-dl替代ytdl-core后,启动音频播放时Player状态始终显示为Buffering,但实际能完整收听歌曲
  • 歌曲结束时状态直接从Playing跳转为Idle
  • 无法通过pause/resume/stop方法控制播放

状态监听代码

player.on('stateChange', (oldState, newState) => {
    console.log(oldState.status, newState.status);
});

完整命令代码

const { SlashCommandBuilder, hideLinkEmbed } = require('@discordjs/builders');
const { createAudioPlayer, NoSubscriberBehavior, joinVoiceChannel, createAudioResource, subscribe, VoiceConnectionStatus } = require('@discordjs/voice');
const ConfigJSON = require('../config.json');
const ytdl = require('ytdl-core');
const playdl = require('play-dl');
const ytsr = require('ytsr');
const { EmbedBuilder } = require('discord.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('music')
        .setDescription('Play a song in a voice channel')
        .addSubcommand(subcommand =>
            subcommand
                .setName('play')
                .setDescription('Play a song in a voice channel')
                .addStringOption(option =>
                    option.setName('query')
                        .setDescription('The song you want to play')
                        .setRequired(true)))
        .addSubcommand(subcommand =>
            subcommand
                .setName('pause')
                .setDescription('Stop the music and leave the voice channel'))
        .addSubcommand(subcommand =>
            subcommand
                .setName('resume')
                .setDescription('Resume the music in the voice channel'))
        .addSubcommand(subcommand =>
            subcommand
                .setName('stop')
                .setDescription('Stop the music and leave the voice channel')),

    async execute(interaction) {
        const subcommand = interaction.options.getSubcommand();
        const YTapiKey = ConfigJSON.YoutubeAPIKey;

        const player = new createAudioPlayer({
            behaviors: {
                noSubscriber: NoSubscriberBehavior.Play,
            },
        });

        if (subcommand === 'play') {
            interaction.deferReply();
            const query = interaction.options.getString('query');
            const opts = { maxResults: 1, key: YTapiKey };

            const queryResult = await ytsr(query);
            const Finalquery = String(await queryResult.items[0].url);
            const streamplay = await playdl.stream(Finalquery);
            const connection = new joinVoiceChannel({
                channelId: interaction.member.voice.channel.id,
                guildId: interaction.guild.id,
                adapterCreator: interaction.guild.voiceAdapterCreator,
                selfDeaf: true,
                selfMute: false,
            });

            connection.subscribe(player);
            const resource = createAudioResource(streamplay.stream, { inputType: streamplay.type });

            let embed = new EmbedBuilder()
                .setTitle(queryResult.items[0].title)
                .setURL(queryResult.items[0].url)
                .setThumbnail(String(queryResult.items[0].bestThumbnail.url))
                .setDescription("Currently playing ....")
                .setFooter({ text: "Requested by " + interaction.user.username, iconURL: interaction.user.avatarURL() })
                .setTimestamp()
                .setColor(0x00AE86)

            player.play(resource);
            await interaction.editReply({ embeds: [embed] });
        }

        if (subcommand === 'pause') {
            console.log("pause");
            console.log(player);
            await player.pause();
            player.on('error', (error) => {
                console.log(error);
            });
        }
        if (subcommand === 'resume') {
            console.log("resume");
            console.log(player.state.status);
        }
        if (subcommand === 'stop') {
            console.log("stop");
            console.log(player.state.status);
        }
        player.on('stateChange', (oldState, newState) => {
            console.log(oldState.status, newState.status);
        });
    }   
}

问题根源

每次执行/music命令时,execute函数都会创建全新的AudioPlayer实例。当调用pause/resume时,操作的是新创建的player,而非正在播放歌曲的那个实例,导致状态不匹配、控制失效。

解决方法

将player和voice connection按服务器(guild)存储,确保同一服务器内的所有子命令操作的是同一个实例:

1. 添加全局存储结构

在模块顶部添加两个Map,用于存储每个服务器的播放器和连接:

const { SlashCommandBuilder, hideLinkEmbed } = require('@discordjs/builders');
const { createAudioPlayer, NoSubscriberBehavior, joinVoiceChannel, createAudioResource, VoiceConnectionStatus } = require('@discordjs/voice');
const ConfigJSON = require('../config.json');
const playdl = require('play-dl');
const ytsr = require('ytsr');
const { EmbedBuilder } = require('discord.js');

// 全局存储每个服务器的播放器和连接
const guildPlayers = new Map();
const guildConnections = new Map();

2. 修改play子命令逻辑

检查当前服务器是否已有播放器和连接,没有则创建并存储:

if (subcommand === 'play') {
    interaction.deferReply();
    const query = interaction.options.getString('query');
    const guildId = interaction.guild.id;

    // 获取或创建语音连接
    let connection = guildConnections.get(guildId);
    if (!connection) {
        connection = joinVoiceChannel({
            channelId: interaction.member.voice.channel.id,
            guildId: guildId,
            adapterCreator: interaction.guild.voiceAdapterCreator,
            selfDeaf: true,
            selfMute: false,
        });
        guildConnections.set(guildId, connection);
    }

    // 获取或创建播放器
    let player = guildPlayers.get(guildId);
    if (!player) {
        player = createAudioPlayer({
            behaviors: {
                noSubscriber: NoSubscriberBehavior.Play,
            },
        });
        guildPlayers.set(guildId, player);

        // 监听状态变化和错误(只绑定一次)
        player.on('stateChange', (oldState, newState) => {
            console.log(oldState.status, newState.status);
            // 播放结束后可选择清理资源
            if (newState.status === 'idle') {
                // connection.destroy();
                // guildConnections.delete(guildId);
                // guildPlayers.delete(guildId);
            }
        });
        player.on('error', error => {
            console.error('播放器错误:', error);
        });
        connection.subscribe(player);
    }

    // 获取音频流并播放
    const opts = { maxResults: 1, key: ConfigJSON.YoutubeAPIKey };
    const queryResult = await ytsr(query);
    const finalQuery = queryResult.items[0].url;
    const streamplay = await playdl.stream(finalQuery);
    const resource = createAudioResource(streamplay.stream, { inputType: streamplay.type });

    const embed = new EmbedBuilder()
        .setTitle(queryResult.items[0].title)
        .setURL(queryResult.items[0].url)
        .setThumbnail(queryResult.items[0].bestThumbnail.url)
        .setDescription("正在播放....")
        .setFooter({ text: `由 ${interaction.user.username} 请求`, iconURL: interaction.user.avatarURL() })
        .setTimestamp()
        .setColor(0x00AE86);

    player.play(resource);
    await interaction.editReply({ embeds: [embed] });
}

3. 修改pause/resume/stop子命令

从全局存储中获取当前服务器的播放器,再执行控制操作:

if (subcommand === 'pause') {
    const player = guildPlayers.get(interaction.guild.id);
    if (player && player.state.status === 'playing') {
        player.pause();
        await interaction.reply('已暂停播放');
    } else {
        await interaction.reply('当前没有正在播放的歌曲');
    }
}

if (subcommand === 'resume') {
    const player = guildPlayers.get(interaction.guild.id);
    if (player && player.state.status === 'paused') {
        player.unpause();
        await interaction.reply('已继续播放');
    } else {
        await interaction.reply('当前没有暂停的歌曲');
    }
}

if (subcommand === 'stop') {
    const guildId = interaction.guild.id;
    const player = guildPlayers.get(guildId);
    const connection = guildConnections.get(guildId);

    if (player) {
        player.stop();
        guildPlayers.delete(guildId);
    }
    if (connection) {
        connection.destroy();
        guildConnections.delete(guildId);
    }
    await interaction.reply('已停止播放并退出语音频道');
}

额外说明

  • 可根据需求添加队列功能,避免重复播放同一首歌
  • 播放结束后清理资源的逻辑可根据机器人使用场景调整,若需支持连续播放则不要立即删除player和connection

内容的提问来源于stack exchange,提问作者EquinoXx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 19:35:04