Discord机器人用ffmpeg跳转播放时出现延迟、音频混乱问题
Discord机器人音频跳转播放问题排查与优化
问题描述
我开发的Discord机器人支持用户从指定时间戳开始播放音频,但当设置的起始时间≥30秒时,会出现播放延迟、音频加速且混乱的问题。我通过ffmpeg_options设置起始时间,想知道问题原因,以及如何优化播放延迟并支持多章节YouTube视频的中间位置播放。
测试结果
以下为同一30秒时长URL的不同起始时间测试数据:
| 输入起始时间(秒) | 播放延迟(秒) | 音频播放时长(秒) |
|---|---|---|
| 0 | 3 | 30 |
| 30 | 10 | 22 |
| 60 | 17 | 17 |
| 120 | 31 | 2 |
| 150 | 120 | <1 |
相关代码
import discord import youtube_dl import asyncio # Suppress noise about console usage from errors youtube_dl.utils.bug_reports_message = lambda: "" ytdl_format_options = { "format": "bestaudio/best", "outtmpl": "%(extractor)s-%(id)s-%(title)s.%(ext)s", "restrictfilenames": True, "noplaylist": False, "yesplaylist": True, "nocheckcertificate": True, "ignoreerrors": False, "logtostderr": False, "quiet": True, "no_warnings": True, "default_search": "auto", "source_address": "0.0.0.0", # Bind to ipv4 since ipv6 addresses cause issues at certain times } ytdl = youtube_dl.YoutubeDL(ytdl_format_options) class YTDLSource(discord.PCMVolumeTransformer): def __init__(self, source: discord.AudioSource, *, data: dict, volume: float = 0.5): super().__init__(source, volume) self.data = data self.title = data.get("title") self.url = data.get("url") @classmethod async def from_url(cls, url, *, loop=None, stream=False, timestamp = 0): ffmpeg_options = { "options": f"-vn -ss {timestamp}"} loop = loop or asyncio.get_event_loop() data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) if "entries" in data: # Takes the first item from a playlist data = data["entries"][0] filename = data["url"] if stream else ytdl.prepare_filename(data) return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) intents = discord.Intents.default() bot = discord.Bot(intents=intents) @bot.slash_command() async def play(ctx, audio: discord.Option(), seconds: discord.Option(), timestamp: discord.Option()): channel = ctx.author.voice.channel voice = await channel.connect() player = await YTDLSource.from_url(audio, loop=bot.loop, stream=True, timestamp=int(timestamp)) voice.play(player) await asyncio.sleep(int(seconds)) await voice.disconnect() token = "token value" bot.run(token)
问题原因分析
- FFmpeg参数位置错误:当前把
-ss放在options中(属于输出阶段参数),FFmpeg会先完整下载从开头到目标时间点的流,再开始编码输出,导致延迟随起始时间增加而线性变长。如果目标时间超过流的实际时长,还会出现音频截断、加速混乱的情况。 - YouTube流式限制:YouTube的音频流采用分段传输,旧版youtube-dl无法直接获取对应分段的地址,FFmpeg需要等待前面的分段加载完成才能定位,进一步加剧延迟。
- 播放断开逻辑不合理:代码中用
await asyncio.sleep(int(seconds))固定时长后断开连接,没有考虑实际播放的起始延迟,导致音频还没播放完整就被强制断开,出现播放时长缩短的问题。
优化方案
1. 调整FFmpeg参数顺序
将-ss移到before_options中(输入阶段参数),让FFmpeg直接定位到目标时间点,无需预加载前面的内容:
@classmethod async def from_url(cls, url, *, loop=None, stream=False, timestamp = 0): ffmpeg_options = { "before_options": f"-ss {timestamp}", "options": "-vn" } loop = loop or asyncio.get_event_loop() data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) if "entries" in data: data = data["entries"][0] filename = data["url"] if stream else ytdl.prepare_filename(data) return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
2. 替换youtube-dl为yt-dlp
youtube-dl已停止维护,yt-dlp对YouTube的流式跳转、多章节支持更完善,安装后修改导入:
import yt_dlp as youtube_dl
其余配置基本兼容,yt-dlp能更高效地获取对应时间点的流地址,减少定位延迟。
3. 优化播放断开逻辑
监听音频播放完成事件,自动断开连接,避免固定时长导致的提前断开:
@bot.slash_command() async def play(ctx, audio: discord.Option(str, description="音频URL或搜索关键词"), timestamp: discord.Option(int, description="起始时间(秒)", default=0)): if not ctx.author.voice: await ctx.respond("你需要先加入语音频道!") return channel = ctx.author.voice.channel voice = await channel.connect() player = await YTDLSource.from_url(audio, loop=bot.loop, stream=True, timestamp=timestamp) def after_playing(error): coro = voice.disconnect() fut = asyncio.run_coroutine_threadsafe(coro, bot.loop) try: fut.result() except Exception as e: print(f"断开连接出错:{e}") voice.play(player, after=after_playing) await ctx.respond(f"正在播放:{player.title},从{timestamp}秒开始")
4. 多章节YouTube视频支持
利用yt-dlp获取视频章节信息,允许用户通过章节名称跳转:
@classmethod async def from_url(cls, url, *, loop=None, stream=False, timestamp = 0, chapter_name=None): ffmpeg_options = { "before_options": f"-ss {timestamp}", "options": "-vn" } loop = loop or asyncio.get_event_loop() # 先获取原始信息,包含章节 data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream, process=False)) # 如果指定了章节名称,匹配对应的起始时间 if chapter_name and "chapters" in data: for chapter in data["chapters"]: if chapter["title"].lower() == chapter_name.lower(): timestamp = chapter["start_time"] ffmpeg_options["before_options"] = f"-ss {timestamp}" break # 处理流信息 data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) if "entries" in data: data = data["entries"][0] filename = data["url"] if stream else ytdl.prepare_filename(data) return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
然后修改play命令,增加章节名称选项:
@bot.slash_command() async def play(ctx, audio: discord.Option(str, description="音频URL或搜索关键词"), timestamp: discord.Option(int, description="起始时间(秒)", default=0), chapter: discord.Option(str, description="章节名称(可选,优先级高于时间戳)", required=False)): if not ctx.author.voice: await ctx.respond("你需要先加入语音频道!") return channel = ctx.author.voice.channel voice = await channel.connect() player = await YTDLSource.from_url(audio, loop=bot.loop, stream=True, timestamp=timestamp, chapter_name=chapter) def after_playing(error): coro = voice.disconnect() fut = asyncio.run_coroutine_threadsafe(coro, bot.loop) try: fut.result() except Exception as e: print(f"断开连接出错:{e}") start_info = f"第「{chapter}」章节" if chapter else f"{timestamp}秒处" await ctx.respond(f"正在播放:{player.title},从{start_info}开始") voice.play(player, after=after_playing)
内容的提问来源于stack exchange,提问作者J Petersen
相关产品推荐
相关产品推荐

