Skip to content

Instantly share code, notes, and snippets.

@SlowLogicBoy
Last active February 21, 2022 13:10
Show Gist options
  • Save SlowLogicBoy/63037d655cf8c667ae09ccebcf1164aa to your computer and use it in GitHub Desktop.
Save SlowLogicBoy/63037d655cf8c667ae09ccebcf1164aa to your computer and use it in GitHub Desktop.
Example on how to join audio and video streams from Youtube using FFmpeg and YoutubeExplode with pipelines.
//Run using dotnet script (https://github.com/filipw/dotnet-script)
#r "nuget: YoutubeExplode, *"
using System.Diagnostics;
using System.IO.Pipes;
using YoutubeExplode;
using YoutubeExplode.Models;
using YoutubeExplode.Models.MediaStreams;
var videoId = "c8vbgNLDNDM";
var outputDir = @".\out";
const string FFmpegPath = @".\ffmpeg.exe";
var youtubeClient = new YoutubeClient();
var youtubeStreamInfo = await youtubeClient.GetVideoMediaStreamInfosAsync(videoId);
var videoStreamInfo = youtubeStreamInfo.Video.WithHighestVideoQuality();
var audioStreamInfo = youtubeStreamInfo.Audio.WithHighestBitrate();
var outputFileName = $"{videoId}.mp4";
var outputFilePath = Path.Combine(outputDir, outputFileName);
//Creates a pipeline stream and downloads media stream to it.
//Thanks to https://mathewsachin.github.io/blog/2017/07/28/ffmpeg-pipe-csharp.html
async Task DownloadToPipeStreamAsync(string pipeName, MediaStreamInfo mediaStreamInfo)
{
using (var pipe = new NamedPipeServerStream(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous))
{
await pipe.WaitForConnectionAsync(); //We need to wait until someone connects to our pipeline
await youtubeClient.DownloadMediaStreamAsync(mediaStreamInfo, pipe); //Download media stream to pipeline
}
}
async Task RunFFMpegAsync()
{
const string videoPipeName = "ffvideo";
const string audioPipeName = "ffaudio";
//-movflags frag_keyframe+empty_moov = https://stackoverflow.com/questions/34123272/ffmpeg-transmux-mpegts-to-mp4-gives-error-muxer-does-not-support-non-seekable#34133803
//-f mp4 = we need to specify which format we want out.
//-i \\.\pipe\pipeName = \\.\pipe\ = we want input to go from pipeline
var processStartInfo = new ProcessStartInfo
{
FileName = Path.GetFullPath(FFmpegPath),
Arguments = $@"-i \\.\pipe\{videoPipeName} -i \\.\pipe\{audioPipeName} -c:v copy -c:a aac -movflags frag_keyframe+empty_moov -f mp4 -",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using (var process = Process.Start(processStartInfo))
{
using (var fileStream = new FileStream(outputFilePath, FileMode.CreateNew))
{
//I don't await tasks directly because they all need to be parallel. Otherwise = deadlock
var audioDownloadTask = DownloadToPipeStreamAsync(audioPipeName, audioStreamInfo);
var videoDownloadTask = DownloadToPipeStreamAsync(videoPipeName, videoStreamInfo);
var outputTask = process.StandardOutput.BaseStream.CopyToAsync(fileStream);
//Wait for all tasks to finnish
await Task.WhenAll(audioDownloadTask, videoDownloadTask, outputTask);
process.StandardOutput.Close();
}
process.WaitForExit();
}
}
await RunFFMpegAsync();
@l-Nuril-l
Copy link

Version 2022

using System.Diagnostics;
using System.IO.Pipes;
using YoutubeExplode;
using YoutubeExplode.Videos.Streams;

var videoId = "c8vbgNLDNDM";
var outputDir = @".\out";

const string FFmpegPath = @".\ffmpeg.exe";

var youtube = new YoutubeClient();
var streamManifest = await youtube.Videos.Streams.GetManifestAsync(videoId);
var videoStreamInfo = streamManifest.GetVideoOnlyStreams().TryGetWithHighestVideoQuality();
var audioStreamInfo = streamManifest.GetAudioOnlyStreams().TryGetWithHighestBitrate();

var outputFileName = $"{videoId}.mp4";
var outputFilePath = Path.Combine(outputDir, outputFileName);

//Creates a pipeline stream and downloads media stream to it.
//Thanks to https://mathewsachin.github.io/blog/2017/07/28/ffmpeg-pipe-csharp.html
async Task DownloadToPipeStreamAsync(string pipeName, IStreamInfo mediaStreamInfo)
{
    using (var pipe = new NamedPipeServerStream(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous))
    {
        await pipe.WaitForConnectionAsync(); //We need to wait until someone connects to our pipeline
        await youtube.Videos.Streams.CopyToAsync(mediaStreamInfo, pipe); //Download media stream to pipeline
    }
}

async Task RunFFMpegAsync()
{
    const string videoPipeName = "ffvideo";
    const string audioPipeName = "ffaudio";
    //-movflags frag_keyframe+empty_moov = https://stackoverflow.com/questions/34123272/ffmpeg-transmux-mpegts-to-mp4-gives-error-muxer-does-not-support-non-seekable#34133803
    //-f mp4 = we need to specify which format we want out.
    //-i \\.\pipe\pipeName = \\.\pipe\ = we want input to go from pipeline
    var processStartInfo = new ProcessStartInfo
    {
        FileName = Path.GetFullPath(FFmpegPath),
        Arguments = $@"-i \\.\pipe\{videoPipeName} -i \\.\pipe\{audioPipeName} -c:v copy -c:a aac -movflags frag_keyframe+empty_moov -f mp4 -",
        UseShellExecute = false,
        RedirectStandardOutput = true,
        CreateNoWindow = true
    };
    using (var process = Process.Start(processStartInfo))
    {
        using (var fileStream = new FileStream(outputFilePath, FileMode.Create))
        {
            //I don't await tasks directly because they all need to be parallel. Otherwise = deadlock
            var audioDownloadTask = DownloadToPipeStreamAsync(audioPipeName, audioStreamInfo);
            var videoDownloadTask = DownloadToPipeStreamAsync(videoPipeName, videoStreamInfo);
            var outputTask = process.StandardOutput.BaseStream.CopyToAsync(fileStream);
            //Wait for all tasks to finnish
            await Task.WhenAll(audioDownloadTask, videoDownloadTask, outputTask);
            process.StandardOutput.Close();
        }
        process.WaitForExit();
    }
}

await RunFFMpegAsync();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment