serenity-rs / songbird

An async Rust library for the Discord voice API
ISC License
384 stars 110 forks source link

Zombie processes for ffmpeg & youtube-dl on Windows #164

Closed ChristopherVR closed 9 months ago

ChristopherVR commented 1 year ago

Songbird version: 0.3.1

Rust version (rustc -V): rustc 1.67.0 (fc594f156 2023-01-24)

Serenity/Twilight version: serenity 0.11.5 (default features enabled)

Output of ffmpeg -version, yt-dlp --version: ffmpeg version 5.1.2-essentials_build-www.gyan.dev Copyright (c) 2000-2022 the FFmpeg developers built with gcc 12.1.0 (Rev2, Built by MSYS2 project) configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-lzma --enable-zlib --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-sdl2 --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-libass --enable-libfreetype --enable-libfribidi --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-libgme --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libtheora --enable-libvo-amrwbenc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-librubberband libavutil 57. 28.100 / 57. 28.100 libavcodec 59. 37.100 / 59. 37.100 libavformat 59. 27.100 / 59. 27.100 libavdevice 59. 7.100 / 59. 7.100 libavfilter 8. 44.100 / 8. 44.100 libswscale 6. 7.100 / 6. 7.100 libswresample 4. 7.100 / 4. 7.100 libpostproc 56. 6.100 / 56. 6.100

Youtube-dl 2023.03.04 (build it myself from latest master)

Description:

I'm using a combination of serenity (slash commmands) and songbird voice receive examples but everytime I attempt to stop the handler youtube-dl and ffmpeg processes still persist. Attempting to receive audio and play music through discord yields in no sound being played if I use the play_source function for the second time. I've had cases where if I do try to play a different song it restarts the previous song?

Steps to reproduce:

My main.rs code:

#[async_trait]
impl EventHandler for Handler {
    async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
        if let Interaction::ApplicationCommand(command) = interaction {
            let _= match command.data.name.as_str() {
                "play" => match commands::music::play::run(&ctx, &command).await {
                    Some(v) => v,
                    None => "".into(),
                },
                _ => "not implemented :(".to_string(),
            };
        }
    }

    async fn ready(&self, ctx: Context, ready: Ready) {
        println!("{} is connected!", ready.user.name);
        let guild_option = ready.guilds.iter().find(|f| f.unavailable);
        if let Some(guild_id) = guild_option {
            let commands = GuildId::set_application_commands(&guild_id.id, &ctx.http, |commands| {
                commands
                    .create_application_command(|command| commands::music::play::register(command))
            })
            .await;
            println!(
                "I now have the following guild slash commands: {:#?}",
                commands
            );
        }
    }
}

#[tokio::main]
async fn main() {
    dotenv().ok();
    let token = env::var("CLIENT_TOKEN").expect("Discord token not found in environment file.");
    let framework = StandardFramework::new().configure(|c| c.prefix("~"));

    let mut client = Client::builder(
        token,
        GatewayIntents::GUILDS
            | GatewayIntents::GUILD_VOICE_STATES
            | GatewayIntents::GUILD_MESSAGES,
    )
    .event_handler(Handler)
    .framework(framework)
    .register_songbird()
    .await
    .expect("Error creating client");

    if let Err(why) = client.start().await {
        println!("Client error: {:?}", why);
    }
}

My play command code:

const SLASH_NAME: &str = "link-or-query";

pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) -> Option<String> {
    println!("Running play command.");

    if let Some(guild_id) = command.guild_id {
        println!("Guild Id found. Command will execute.");
        let song = command
            .data
            .options
            .iter()
            .find(|&f| f.name.eq(SLASH_NAME))
            .unwrap()
            .value
            .as_ref()
            .unwrap();

        if let Value::String(song) = song {
            println!("Song to play - {}", song);
            let manager = songbird::get(ctx).await.expect(
                "Failed to retrieve Songbird. Check if Songbird is registered on ClientBuilder.",
            );

            let channel_id = &ctx
                .cache
                .guild(guild_id)
                .unwrap()
                .voice_states
                .get(&command.user.id)
                .and_then(|voice_state| voice_state.channel_id)
                .expect("User needs to be connected to a voice channel.");

            let (handler_lock, conn_result) = manager.join(guild_id, channel_id.0).await;

            let _res = command.defer(&ctx.http).await;
            if let Ok(_) = conn_result {
                let mut handler = handler_lock.lock().await;

                command
                    .create_followup_message(&ctx.http, |f| {
                        f.ephemeral(true).content("Searching for song.")
                    })
                    .await
                    .unwrap();

                let obj = match songbird::ytdl(format!("ytsearch1:{}", &song)).await {
                    Ok(source) => Some(source),
                    Err(why) => {
                        println!("An error ocurred {:?}", why);
                        None
                    }
                };
                if let Some(song) = obj {
                    let response = handler.play_source(song.into());

                    match response.set_volume(1.0) {
                        Ok(_) => (),
                        Err(err) => println!("Failed to adjust song volume - {:?}", err),
                    } // Default to full volume.

                    println!("Response for song object - {:?}", response);
                    command
                        .create_followup_message(&ctx.http, |f| {
                            f.ephemeral(true).content("Song played!.")
                        })
                        .await
                        .unwrap();
                } else {
                    command
                        .create_followup_message(&ctx.http, |f| {
                            f.ephemeral(true).content("Unable to handle song request.")
                        })
                        .await
                        .unwrap();
                }
            }
        }

        return None;
    } else {
        return Some("Unable to execute command. User is not connected to a channel".into());
    }
}
anpage commented 1 year ago

I've been banging my head against this one. It happens in Linux too.

I'm not sure about the zombie processes, but the strange behavior with yt-dlp seems to be caused by its use of --Frag# files when downloading. The two instances of yt-dlp try to use the same file and if the old one is still around when the new one starts to stream, you get the same song.

ChristopherVR commented 1 year ago

Yep I've also noticed that. Deleting the frag files before the new one starts to stream resolves the issue where you get the same song, but this has been quite frustrating trying to find the root cause.

FelixMcFelix commented 1 year ago

It's disappointing seeing that zombie process issues have resurfaced; it's been one of those things that seems to crop up again at random long after you think it's dead (ha!). The current changes on next were designed to solve this issue by doing away with long-lived subprocesses, but I appreciate it's a lot of work to convert to since it's tied to Serenity's next branch.

Skarlett commented 1 year ago

Yeah, we're facing a similar problem with zombie processes (NixOS).

While some processes are marked as zombies (by the OS), some ffmpeg streams are also marked as still running despite being not being used by songbird any longer

FelixMcFelix commented 9 months ago

Closing as v0.4.x onwards no longer make use of ffmpeg or child processes for audio processing.