Skip to content

Instantly share code, notes, and snippets.

@NaokiStark
Last active July 1, 2019 13:59
Show Gist options
  • Save NaokiStark/6b6a3fb6784ec8bc694b566818fc8cc8 to your computer and use it in GitHub Desktop.
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
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();
}
}
}
@NaokiStark
Copy link
Author

NaokiStark commented Jun 29, 2019

Use:

var VDecoder = new VideoDecoder("Filename.mp4", 1280, 720);
VDecoder.Decode();
VDecoder.WaitForDecoder();
// ...

// Example: use a decoded frame to image (this needs to decode to RBG32 to work, check #107)
byte[] frame = VDecoder.GetFrame(1000);
var ms = new MemoryStream(frame);
var image = Image.FromStream(ms);

image.Save("image.jpg");

// ...

// Always dispose decoder
VDecoder.Dispose();

@NaokiStark
Copy link
Author

NaokiStark commented Jul 1, 2019

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