Created
December 30, 2022 16:26
-
-
Save IsaMorphic/0bbc913905afc553e3a21964cd5bb49a to your computer and use it in GitHub Desktop.
FFMediaToolkit Example - Remux M4V+MP3 to MP4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using FFMediaToolkit.Audio; | |
using FFMediaToolkit.Decoding; | |
using FFMediaToolkit.Encoding; | |
using FFMediaToolkit.Graphics; | |
// File paths, grab from anywhere like command-line args, config file, etc. | |
string inputVideoFilePath = "video-mute.m4v"; | |
string inputAudioFilePath = "audio.mp3"; | |
string outputMediaFilePath = "video-dubbed.mp4"; | |
// Open input media containers. | |
// Expects the following inputs: | |
// - M4V video container | |
// - MP3 audio container | |
using var inputVideoFile = MediaFile.Open( | |
Path.GetFullPath(inputVideoFilePath), | |
new MediaOptions { StreamsToLoad = MediaMode.Video } // Tell FFMediaToolkit we are only interested in video data from this container. | |
); | |
using var inputAudioFile = MediaFile.Open( | |
Path.GetFullPath(inputAudioFilePath), | |
new MediaOptions { StreamsToLoad = MediaMode.Audio } // Tell FFMediaToolkit we are only interested in audio data from this container. | |
); | |
var videoInfo = inputVideoFile.Video.Info; // Grab codec information for input video stream. | |
var audioInfo = inputAudioFile.Audio.Info; // IMPORTANT: Grab codec information for input audio stream; contains crucial parameter values. | |
// Now we initialize the output container | |
using var outputMediaFile = MediaBuilder.CreateContainer(Path.GetFullPath(outputMediaFilePath)) | |
.WithVideo( // single video stream with same params as input video, same codec | |
new VideoEncoderSettings(videoInfo.FrameSize.Width, videoInfo.FrameSize.Height) | |
{ FramerateRational = videoInfo.RealFrameRate } // Just in case contents are variable or non-integer framerate | |
) | |
// THIS IS THE CRITICAL SECTION OF CONFIGURATION!! | |
// All audio codecs in FFmpeg have specific restrictions on which sample rates, formats, and number of channels they can use. | |
// Additionally, like video streams, FFmpeg splits audio streams into "frames". | |
// These audio frames each contain the same number of samples, as specified by SamplesPerFrame in FFMediaToolkit. | |
// SampleFormat and SamplesPerFrame must be configured manually at the moment when using FFMediaToolkit for the | |
// library to properly encode audio. | |
// IMPORTANT NOTE: | |
// FFMediaTookit's API exposes an AudioData structure, much like ImageData. It contains members to extract and update audio frame data | |
// as float[][], where the first index is for which audio channel you want to access, and the second is for which sample in that channel. | |
// To read audio frame data, use the methods AudioData.GetChannelData() & AudioData.GetSampleData(). | |
// To modify audio frame data, use the methods AudioData.UpdateChannelData() & AudioData.UpdateSampleData(). | |
.WithAudio( | |
new AudioEncoderSettings(audioInfo.SampleRate, audioInfo.NumChannels, AudioCodec.MP3) | |
{ | |
SampleFormat = audioInfo.SampleFormat, | |
SamplesPerFrame = audioInfo.SamplesPerFrame, | |
} | |
) | |
.Create(); // Whew! That was a lot of explanation! Let's finish this! | |
// Store references to input & output streams in variables with shorthand names. | |
var videoIn = inputVideoFile.Video; | |
var audioIn = inputAudioFile.Audio; | |
var videoOut = outputMediaFile.Video; | |
var audioOut = outputMediaFile.Audio; | |
// Next, to properly mux the file with audio and video in sync, we need to use one stream as a "pacer" and the other as a "follower". | |
// Let's use the video as the "pacer". | |
bool stillMoreAudio = true; | |
while (videoIn.TryGetNextFrame(out ImageData imageData)) // Get next video frame as long as its available. | |
// This sets the video stream ahead of the audio stream. | |
{ | |
videoOut.AddFrame(imageData, videoIn.Position); // Add it to the output container. | |
// Now, because audio frames are shorter than video frames, we remux as many as we can until we catch up to the video stream again. | |
while(audioIn.Position < videoIn.Position && (stillMoreAudio = audioIn.TryGetNextFrame(out AudioData audioData))) | |
{ | |
// If the input SamplesPerFrame were different than the output SamplesPerFrame, | |
// you'd need to implement some extra buffering logic here to redistribute the data. | |
outputMediaFile.Audio.AddFrame(audioData, audioIn.Position); | |
} | |
if (!stillMoreAudio) break; // in case there's no more audio, we bail out! | |
} | |
// Stop at end of shortest input file. | |
return; // Now we're done! Because we prefixed our IDisposable declarations with "using", application will clean up for us. | |
// Don't forget to do this manually if necessary! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment