pythonaudiodiscord.pyyoutubepytube

Streaming audio content from YouTube to Discord


I'm developing a Discord bot using discord.py, and as part of that bot I am tasked with creating music functionality. I have actually done that, and the bot will play songs using the pytube module. It does this by downloading the best audio source, converting it to .wav (so that Discord can understand it) then opening that file and playing it back through the voice client for everyone in the voice call to hear. The trouble with this is that it is very slow due to having to download the full audio before beginning to play. This is the code as it stands currently:

    # Plays a Youtube object in the voice call.
    async def play_youtube_object(self, yt:YouTube, interaction:discord.Interaction, voice_client):
        try:
            # Extract audio stream with highest audio bitrate
            audio_stream = yt.streams.filter(file_extension="webm").order_by('abr').desc().first()
            audio_filename = os.path.abspath(rf"./data/music cache/{yt.author}{hex(random.randint(1,999999))[2:]}")
            audio_stream.download(filename=audio_filename+".webm")

            # Convert from .webm to .wav
            audio_f = moviepy.AudioFileClip(audio_filename+".webm")
            audio_f.write_audiofile(audio_filename+".wav", fps=48000) # Discord requires 48kHz, 44.1kHz causes it to speed up by 8.125%.
            remove_file(audio_filename + ".webm") # Remove the .webm since we're now done with it

            # Open the downloaded audio file in binary mode and create a PCMAudio object which can be interpreted by discord
            f = open(audio_filename+".wav", "rb")
            source = PCMAudio(f)

            # Play the audio
            voice_client.play(source)

            # Wait until the song is finished before ending the function
            while voice_client.is_playing() or voice_client.is_paused():
                await asyncio.sleep(1)
            f.close()
        except ...

My potential solution to this (if it's possible) is to stream the audio directly from YouTube, converting it to the PCMAudio source and feeding it to Discord live. The trouble is, I have no idea where to even start with implementing that. Is it possible? If so, where should I start research for that? Is there a module that might help?

Effectively the goal is to have close to instantaneous feedback: a pytube YouTube object is parsed into the play_youtube_object subroutine, and audio begins playing for the user in Discord straight afterwards (or at least not with up to a 20 second delay as it currently is).

Any help or pointers appreciated, thank you in advance! :D


Solution

  • Solved! Thanks to some help pointing me in the right direction from the comments. Using yt-dlp and piping using ffmpeg worked brilliantly and knocked out that pesky latency (Roughly 3 seconds now, which is not bad at all).

    This is the working code, currently a bit of a mess but I figured I'd come and wrap up this post before I continue working on it:

        async def play_youtube_url(self, url, interaction:discord.Interaction, voice_client):
            ydl_opts = {
                'format': 'bestaudio/best',
                'quiet': True,
                'noplaylist': True
            }
    
            try:
                with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                    info_dict = ydl.extract_info(url, download=False)
                    audio_url = info_dict['url']
    
                ffmpeg_options = {
                    'options': '-vn'
                }
    
                source = FFmpegPCMAudio(audio_url, **ffmpeg_options)
                voice_client.play(source)
    
                while voice_client.is_playing() or voice_client.is_paused():
                    await asyncio.sleep(1)
            except Exception as e:
                await interaction.followup.send(f"An error occured: {e}")
    

    Thanks to yt-dlp the program will be able to handle many more audio sources, just not those with DRM protection (like Spotify), so I might have to see if I can find a way around that, or if anyone knows anything then please comment :).