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

使用discord.py与yt-dlp开发Discord音乐机器人,歌曲播放几秒即停止

Discord语音机器人播放几秒即停的问题修复

问题场景

我是Python新手,开发Discord语音机器人时遇到问题:机器人进入语音频道播放歌曲,仅几秒后就停止。之前用已停止维护的youtube-dl也出现同样问题,代码如下:

import discord
from discord.ext import commands
import yt_dlp
import config
import asyncio

bot = commands.Bot(command_prefix='m!', intents=discord.Intents.all())

#Bot startup

@bot.event
async def on_ready():
    print(f'{bot.user} ready')

ffmpeg_options = {'options': '-vn'}

ydl_opts = {
    'format': 'bestaudio/best',
    'quiet': False,
    'noplaylist': False,
    'ignoreerrors': True,
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',
        'preferredcodec': 'mp3',
        'preferredquality': '192',
        
    }],
    
}

class Queue:
    def __init__(self):
        self.queue = []

    def add(self, item):
        self.queue.append(item)

    def remove(self):
        if self.queue:
            return self.queue.pop(0)
        return None

queue = Queue()


@bot.command()
async def ping(ctx):
    await ctx.send("!Pong")

@bot.command()
async def join(ctx):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.send("¡Necesitas estar en un canal de voz!")
        return
    
    voice_channel = ctx.author.voice.channel
    if ctx.voice_client is not None:
        await ctx.voice_client.move_to(voice_channel)
    else:
        await voice_channel.connect()
    await ctx.send(f"Me he unido a {voice_channel}")


@bot.command()
async def leave(ctx):
    if ctx.voice_client is not None:
        await ctx.voice_client.disconnect()
        await ctx.send("Bye Bye")
    else:
        await ctx.send("No estoy en un canal de voz")


#Using play

@bot.command()
async def play(ctx, *, query):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.send("¡Necesitas estar en un canal de voz para usar este comando!")
        return
    
    voice_channel = ctx.author.voice.channel
    
    try:
        vc = ctx.voice_client
        if not vc:
            await voice_channel.connect()
            vc = ctx.voice_client

        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            
            info = ydl.extract_info(f"ytsearch:{query}", download=False)
            url = info['entries'][0]['url']

            queue.add(url)
            if not vc.is_playing():
                play_next(ctx)
                await ctx.send(f"Reproduciendo: {info['entries'][0]['title']}")
            else:
                await ctx.send(f"{info['entries'][0]['title']} añadida a la cola de reproducción")
    except Exception as e:
        print(f"Error al reproducir audio: {e}")
        await ctx.send("Ocurrió un error al reproducir el audio.")


@bot.command()
async def skip(ctx):
    if ctx.voice_client is None or not ctx.voice_client.is_playing():
        await ctx.send("No hay nada reproduciéndose que se pueda saltar.")
        return

    ctx.voice_client.stop()
    await ctx.send("Canción skipped")
    play_next(ctx)

def play_next(ctx):
    url = queue.remove()
    if url:
        ctx.voice_client.play(discord.FFmpegPCMAudio(url), after=lambda e: play_next(ctx))


@bot.command()
async def disconnect(ctx):
    if ctx.voice_client is None:
        await ctx.send("No estoy en un canal de voz.")
        return
    

    await ctx.voice_client.disconnect()
    await ctx.send("Me he desconectado del canal de voz.")


#Disconnect when nobody is in the vc

@bot.event
async def on_voice_state_update(member, before, after):
    if bot.user in member.guild.voice_channels and len(member.guild.voice_channels) == 1:
        await asyncio.sleep(20)
        if len(member.guild.voice_channels) == 1 and bot.user in member.guild.voice_channels:
            await member.guild.voice_client.disconnect()


bot.run(config.TOKEN)

问题修复方案

1. 修复同步播放回调的线程问题

play_next是同步函数,直接在after回调里调用会阻塞事件循环,导致音频播放中断。需要改为异步函数,并通过线程安全的方式调用:

async def play_next(ctx):
    url = queue.remove()
    if url:
        # 传入FFmpeg配置确保正确处理音频流
        source = discord.FFmpegPCMAudio(url, **ffmpeg_options)
        # 用线程安全方法调用异步函数
        ctx.voice_client.play(source, after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx), bot.loop))

同时修改play和skip命令中的调用方式为异步:

  • 在play命令中:await play_next(ctx)
  • 在skip命令中:await play_next(ctx)

2. 启用FFmpeg配置参数

之前定义的ffmpeg_options没有传入FFmpegPCMAudio,导致未禁用视频流,占用资源引发播放中断。修复后已在上面的play_next中传入**ffmpeg_options。

3. 优化yt-dlp配置

调整格式优先级、添加超时设置,提升音频链接的稳定性和兼容性:

ydl_opts = {
    'format': 'bestaudio[ext=mp3]/bestaudio/best',
    'quiet': True,
    'noplaylist': False,
    'ignoreerrors': True,
    'extractaudio': True,
    'audioformat': 'mp3',
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',
        'preferredcodec': 'mp3',
        'preferredquality': '192',
    }],
    'default_search': 'ytsearch',
    'socket_timeout': 10,
}

4. 修复无人时自动断开的逻辑

原代码错误判断服务器语音频道总数,应检查机器人所在频道的成员数量(排除机器人自身):

@bot.event
async def on_voice_state_update(member, before, after):
    vc = member.guild.voice_client
    if vc is None:
        return
    # 统计频道内非机器人成员数量
    human_members = [m for m in vc.channel.members if not m.bot]
    if len(human_members) == 0:
        await asyncio.sleep(20)
        # 再次确认后断开
        human_members = [m for m in vc.channel.members if not m.bot]
        if len(human_members) == 0 and vc.is_connected():
            await vc.disconnect()

5. 增强队列类的可用性

给队列类添加空判断方法,方便后续逻辑处理:

class Queue:
    def __init__(self):
        self.queue = []

    def add(self, item):
        self.queue.append(item)

    def remove(self):
        if self.queue:
            return self.queue.pop(0)
        return None
    
    def is_empty(self):
        return len(self.queue) == 0

完整修复后代码

import discord
from discord.ext import commands
import yt_dlp
import config
import asyncio

bot = commands.Bot(command_prefix='m!', intents=discord.Intents.all())

@bot.event
async def on_ready():
    print(f'{bot.user} ready')

ffmpeg_options = {'options': '-vn'}

ydl_opts = {
    'format': 'bestaudio[ext=mp3]/bestaudio/best',
    'quiet': True,
    'noplaylist': False,
    'ignoreerrors': True,
    'extractaudio': True,
    'audioformat': 'mp3',
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',
        'preferredcodec': 'mp3',
        'preferredquality': '192',
    }],
    'default_search': 'ytsearch',
    'socket_timeout': 10,
}

class Queue:
    def __init__(self):
        self.queue = []

    def add(self, item):
        self.queue.append(item)

    def remove(self):
        if self.queue:
            return self.queue.pop(0)
        return None
    
    def is_empty(self):
        return len(self.queue) == 0

queue = Queue()


@bot.command()
async def ping(ctx):
    await ctx.send("!Pong")

@bot.command()
async def join(ctx):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.send("¡Necesitas estar en un canal de voz!")
        return
    
    voice_channel = ctx.author.voice.channel
    if ctx.voice_client is not None:
        await ctx.voice_client.move_to(voice_channel)
    else:
        await voice_channel.connect()
    await ctx.send(f"Me he unido a {voice_channel}")


@bot.command()
async def leave(ctx):
    if ctx.voice_client is not None:
        await ctx.voice_client.disconnect()
        await ctx.send("Bye Bye")
    else:
        await ctx.send("No estoy en un canal de voz")


@bot.command()
async def play(ctx, *, query):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.send("¡Necesitas estar en un canal de voz para usar este comando!")
        return
    
    voice_channel = ctx.author.voice.channel
    
    try:
        vc = ctx.voice_client
        if not vc:
            await voice_channel.connect()
            vc = ctx.voice_client

        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            info = ydl.extract_info(query, download=False)
            # 处理搜索结果和直接链接的情况
            if 'entries' in info:
                info = info['entries'][0]
            url = info['url']
            title = info['title']

            queue.add(url)
            if not vc.is_playing():
                await play_next(ctx)
                await ctx.send(f"Reproduciendo: {title}")
            else:
                await ctx.send(f"{title} añadida a la cola de reproducción")
    except Exception as e:
        print(f"Error al reproducir audio: {e}")
        await ctx.send("Ocurrió un error al reproducir el audio.")


@bot.command()
async def skip(ctx):
    if ctx.voice_client is None or not ctx.voice_client.is_playing():
        await ctx.send("No hay nada reproduciéndose que se pueda saltar.")
        return

    ctx.voice_client.stop()
    await ctx.send("Canción skipped")
    await play_next(ctx)

async def play_next(ctx):
    url = queue.remove()
    if url:
        source = discord.FFmpegPCMAudio(url, **ffmpeg_options)
        ctx.voice_client.play(source, after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx), bot.loop))


@bot.command()
async def disconnect(ctx):
    if ctx.voice_client is None:
        await ctx.send("No estoy en un canal de voz.")
        return
    
    await ctx.voice_client.disconnect()
    await ctx.send("Me he desconectado del canal de voz.")


@bot.event
async def on_voice_state_update(member, before, after):
    vc = member.guild.voice_client
    if vc is None:
        return
    human_members = [m for m in vc.channel.members if not m.bot]
    if len(human_members) == 0:
        await asyncio.sleep(20)
        human_members = [m for m in vc.channel.members if not m.bot]
        if len(human_members) == 0 and vc.is_connected():
            await vc.disconnect()


bot.run(config.TOKEN)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:35:54