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

Discord机器人用ffmpeg跳转播放时出现延迟、音频混乱问题

Discord机器人音频跳转播放问题排查与优化

问题描述

我开发的Discord机器人支持用户从指定时间戳开始播放音频,但当设置的起始时间≥30秒时,会出现播放延迟、音频加速且混乱的问题。我通过ffmpeg_options设置起始时间,想知道问题原因,以及如何优化播放延迟并支持多章节YouTube视频的中间位置播放。

测试结果

以下为同一30秒时长URL的不同起始时间测试数据:

输入起始时间(秒)播放延迟(秒)音频播放时长(秒)
0330
301022
601717
120312
150120<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)

问题原因分析

  1. FFmpeg参数位置错误:当前把-ss放在options中(属于输出阶段参数),FFmpeg会先完整下载从开头到目标时间点的流,再开始编码输出,导致延迟随起始时间增加而线性变长。如果目标时间超过流的实际时长,还会出现音频截断、加速混乱的情况。
  2. YouTube流式限制:YouTube的音频流采用分段传输,旧版youtube-dl无法直接获取对应分段的地址,FFmpeg需要等待前面的分段加载完成才能定位,进一步加剧延迟。
  3. 播放断开逻辑不合理:代码中用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 06:18:24