使用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
相关产品推荐
相关产品推荐

