How would I stream audio from pytube to FFMPEG and discord.py without downloading and converting beforehand

Viewed 10881

I have a discord.py bot setup in a small server of mine. I have setup a music command that downloads a song from YouTube and outputs it into a VC. At the moment, the command downloads the full song, converts it, and then outputs it into a VC, but this process is heavily slow. How would I go about streaming the audio directly into the VC? I'm open to use youtube_dl in place of pytube3. I don't care much about smaller code optimizations as this is just a small bot for a few friends and me.

Thanks for any input!

@bot.command()
async def play(ctx, *song):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.send("You aren't in a VC!")
        return
    print(song) #debugging
    os.system("rm music.mp3")
    ydl_opts = {
        'noplaylist': True,        
        'outtmpl': 'music',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '128',
        }],
    'format': '139',  
    }
    youtube = pytube.YouTube(str(song).strip("(,)'"))
    video = youtube.streams.filter(only_audio=True).first()
    await ctx.send("downloading")
    video.download(filename="music")
    await ctx.send("converting...")
    os.system("ffmpeg -i music.mp4 -map 0:a:0 -b:a 96k music.mp3")
    
    voice_channel = ctx.author.voice.channel
    vc = await voice_channel.connect()
    vc.play(discord.FFmpegPCMAudio('music.mp3'), after=lambda e: print('done', e))
    while vc.is_playing():
        await asyncio.sleep(1)
    await ctx.voice_client.disconnect()
1 Answers

You're already using youtube_dl (judging by your ydl_opts var). What you can do is:

  • Installing youtube_dl (pip install youtube-dl) if you don't have it.
  • Install requests (pip install requests)
  • Extract the video info:
    from youtube_dl import YoutubeDL
    from requests import get
    
    #Get videos from links or from youtube search
    def search(query):
        with YoutubeDL({'format': 'bestaudio', 'noplaylist':'True'}) as ydl:
            try: requests.get(arg)
            except: info = ydl.extract_info(f"ytsearch:{arg}", download=False)['entries'][0]
            else: info = ydl.extract_info(arg, download=False)
        return (info, info['formats'][0]['url'])
    
  • Make the bot join the channel:
    async def join(ctx, voice):
        channel = ctx.author.voice.channel
    
        if voice and voice.is_connected():
            await voice.move_to(channel)
        else:
            voice = await channel.connect() 
    
  • Play the video:
    from discord import FFmpegPCMAudio
    from discord.ext import commands
    from discord.utils import get
    
    
    @bot.command()
    async def play(ctx, *, query):
        #Solves a problem I'll explain later
        FFMPEG_OPTS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'}
    
        video, source = search(query)
        voice = get(bot.voice_clients, guild=ctx.guild)
    
        await join(ctx, voice)
        await ctx.send(f'Now playing {info['title']}.')
    
        voice.play(FFmpegPCMAudio(source, **FFMPEG_OPTS), after=lambda e: print('done', e))
        voice.is_playing()
    
  • To know what the video variable contains, you can print it.
  • However, streaming audio causes a known issue, explained here. To solve this problem, you have to use that FFMPEG_OPTS variable. It will reconnect the bot to the source so it's still able to stream the video, when this happens, you'll have a strange message in your terminal that you don't need to be worried of.
  • Note that there's no error management, you'll have to do it yourself.
Related