Last active
July 1, 2019 13:59
-
-
Save NaokiStark/6b6a3fb6784ec8bc694b566818fc8cc8 to your computer and use it in GitHub Desktop.
Video decoder (gets raw frames in rawformat in BGR32) using FFmpeg binary with managed code | It works on XNA and Monogame and Decodes all video supported in FFmpeg without audio
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 System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Text; | |
using System.Threading; | |
namespace App.Video | |
{ | |
public class VideoDecoder : IDisposable | |
{ | |
public static VideoDecoder Instance = null; | |
/// <summary> | |
/// Raw Video Buffer | |
/// </summary> | |
public byte[][] buffer = new byte[3][]; | |
/// <summary> | |
/// Buffer AS IS [time, frame] | |
/// </summary> | |
public SortedDictionary<long, byte[]> frameList = new SortedDictionary<long, byte[]>(); | |
/// <summary> | |
/// Decoded frames | |
/// </summary> | |
public int frameIndex; | |
public int VIDEOWIDTH = 854; | |
public int VIDEOHEIGHT = 480; | |
int bytesToRead = 0; | |
int bytesPosition = 0; | |
/// <summary> | |
/// Buffer index | |
/// </summary> | |
public int BufferIndex; | |
/// <summary> | |
/// Last rendered frame requested | |
/// </summary> | |
public int lastFrameId = 0; | |
/// <summary> | |
/// Filename | |
/// </summary> | |
public string FileName { get; private set; } | |
/// <summary> | |
/// Decoding flag | |
/// </summary> | |
public bool Decoding { get; set; } | |
/// <summary> | |
/// FFmpeg | |
/// </summary> | |
Process ffmpegProc; | |
/// <summary> | |
/// Initializes VideoDecoder with 854x480 resolution | |
/// </summary> | |
/// <param name="filename">Video Path</param> | |
public VideoDecoder(string filename) | |
{ | |
Instance = this; | |
FileName = filename; | |
} | |
/// <summary> | |
/// Initializes VideoDecoder with custom resolution | |
/// </summary> | |
/// <param name="filename">Video Path</param> | |
/// <param name="width">Decoded Width</param> | |
/// <param name="height">Decoded height</param> | |
public VideoDecoder(string filename, int width, int height) | |
{ | |
Instance = this; | |
VIDEOWIDTH = width; | |
VIDEOHEIGHT = height; | |
FileName = filename; | |
} | |
/// <summary> | |
/// Begin decoding | |
/// </summary> | |
public void Decode() | |
{ | |
// I've used Thread instead Task, 'z is more flexible, but is a little more dangerous and unsafe | |
Thread vThread = new Thread(new ThreadStart(ThreadedDecoder)); | |
vThread.Start(); | |
} | |
/// <summary> | |
/// Decoder | |
/// </summary> | |
private void ThreadedDecoder() | |
{ | |
// Process info | |
ProcessStartInfo startInfo = new ProcessStartInfo | |
{ | |
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, | |
FileName = "ffmpeg.exe", | |
Arguments = $"-hide_banner -loglevel panic -i \"{FileName}\" -s {VIDEOWIDTH}x{VIDEOHEIGHT} -an -vf scale={VIDEOWIDTH}:{VIDEOHEIGHT}:force_original_aspect_ratio=decrease -bufsize 3 -maxrate 1600k -preset ultrafast -r 30 -framerate 30 -f rawvideo -pix_fmt bgr32 pipe:1", | |
RedirectStandardError = true, | |
RedirectStandardOutput = true, | |
UseShellExecute = false, | |
CreateNoWindow = true, | |
StandardOutputEncoding = Encoding.UTF8, | |
}; | |
ffmpegProc = new Process | |
{ | |
StartInfo = startInfo | |
}; | |
ffmpegProc.Start(); | |
//STDOUT | |
var output = ffmpegProc.StandardOutput; | |
//STDERR | |
var err = ffmpegProc.StandardError; | |
//Decoding flag | |
Decoding = true; | |
// | |
for (int a = 0; a < buffer.Length; a++) | |
{ | |
buffer[a] = new byte[VIDEOWIDTH * VIDEOHEIGHT * 4]; | |
} | |
int current = 4; | |
while (!ffmpegProc.HasExited) | |
{ | |
while (current > 0) | |
{ | |
// Waits if buffer is full | |
while(frameList.Count >= 35) | |
{ | |
Thread.Sleep(1); | |
} | |
// Get the frame | |
while (bytesToRead < buffer[BufferIndex].Length) | |
{ | |
current = output.BaseStream.Read(buffer[BufferIndex], | |
bytesPosition, buffer[BufferIndex].Length - bytesPosition); | |
if(current == 0) | |
{ | |
break; | |
} | |
bytesPosition += current; | |
bytesToRead += current; | |
} | |
if (current != 0) | |
{ | |
// Copy | |
byte[] frm = new byte[buffer[BufferIndex].Length]; | |
buffer[BufferIndex].CopyTo(frm, 0); | |
// Add to buffer | |
frameList.Add(frameIndex * (1000 / 30), frm); | |
frameIndex++; | |
BufferIndex++; | |
if (BufferIndex >= buffer.Length) BufferIndex = 0; | |
bytesToRead = 0; | |
bytesPosition = 0; | |
output.BaseStream.Flush(); | |
} | |
} | |
} | |
Decoding = false; | |
ffmpegProc.Close(); | |
} | |
/// <summary> | |
/// Waits for first buffer fill, useful for timed framing | |
/// </summary> | |
public void WaitForDecoder() | |
{ | |
while(frameIndex < 32) | |
{ | |
Thread.Sleep(1); | |
} | |
} | |
/// <summary> | |
/// Gets debug info | |
/// </summary> | |
/// <returns></returns> | |
public string GetDebugInfo() | |
{ | |
return $"LastFrameInBuffer:{lastFrameId} \nDecodedFrames:{frameIndex} \nFramesInBuffer:{frameList.Count} \nVideoFramerate:30 \n"; | |
} | |
/// <summary> | |
/// Gets frame for desired time in milliseconds | |
/// </summary> | |
/// <param name="time">Frame time in milliseconds</param> | |
/// <returns>byte[] BGR32 frame | null if is not decoding or something bad happens (lost frames|empty buffer)</returns> | |
public byte[] GetFrame(long time) | |
{ | |
if (!Decoding) | |
return null; | |
if (frameList.Keys.Count < 1) | |
return null; | |
try | |
{ | |
long key = 0; | |
int frameInd = 0; | |
for (int a = 0; a < time + 100; a++) | |
{ | |
key = a * (1000 / 30); | |
if (key > time) | |
{ | |
key = Math.Max((a - 1) * (1000 / 30), 0); | |
frameInd = a; | |
break; | |
} | |
} | |
foreach (var s in frameList.Where(kv => kv.Key <= key).ToList()) | |
{ | |
frameList[s.Key] = null; | |
frameList.Remove(s.Key); | |
} | |
key = Math.Max((frameInd) * (1000 / 30), 0); | |
lastFrameId = (int)key; | |
if (!frameList.ContainsKey(key)) | |
return null; | |
var frame = frameList[key]; | |
return frame; | |
} | |
catch | |
{ | |
return null; | |
} | |
} | |
/// <summary> | |
/// Stop decoding and disposes | |
/// </summary> | |
public void Dispose() | |
{ | |
Decoding = false; | |
ffmpegProc.Close(); | |
} | |
} | |
} |
Also, you can use it in XNA|Monogame (I think this is working on Mono with linux ffmpeg binary)
By default uses BGR32
//Create a Texture2D
var videoCanvas = new Texture2D(Graphics, 800, 600);
var VDecoder = new VideoDecoder("Filename.mp4");
VDecoder.Decode();
VDecoder.WaitForDecoder(); //This is optional
// ...
public void Draw(GameTime gameTime){
byte[] videoFrame = VDecoder.GetFrame(gameTime.TotalGameTime.TotalMilliseconds); //Or time what you want, notice this has not a seek function, it make a procedural read filling a buffer
videoCanvas.SetData(videoFrame);
// Render Texture2D as usually
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use: