using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; using Tango.Core; using Tango.Core.Helpers; using Tango.Core.IO; namespace Tango.FSE.Common.RemoteDesktop { public class Mp4VideoEncoder : ExtendedObject, IDisposable { private class QueueItem { public BitmapSource Frame { get; set; } public bool IsComplete { get; set; } } private Thread _queueThread; private ProducerConsumerQueue _framesQueue; private TemporaryFolder _tempFolder; private int _totalFrames; private String _ffmpegPath; public event EventHandler> Progress; public bool IsStarted { get; set; } public Mp4VideoEncoder() { _ffmpegPath = Path.Combine(AssemblyHelper.GetCurrentAssemblyFolder(), "ffmpeg.exe"); } private void QueueThreadMethod() { _tempFolder = TemporaryManager.CreateFolder(); while (true) { var item = _framesQueue.BlockDequeue(); if (!item.IsComplete) { String file = Path.Combine(_tempFolder, "image" + (_totalFrames++) + ".png"); using (FileStream fs = new FileStream(file, FileMode.Create)) { PngBitmapEncoder enc = new PngBitmapEncoder(); enc.Frames.Add(BitmapFrame.Create(item.Frame)); enc.Save(fs); } } else { while (_framesQueue.Count > 0) { _framesQueue.BlockDequeue(); } break; } } } public void Start() { if (!IsStarted) { _framesQueue = new ProducerConsumerQueue(); IsStarted = true; _totalFrames = 0; _queueThread = new Thread(QueueThreadMethod); _queueThread.IsBackground = true; _queueThread.Start(); } } public void Cancel() { Stop(); ClearTemporaryFiles(); } public void Stop() { if (IsStarted) { _framesQueue.BlockEnqueue(new QueueItem() { IsComplete = true }); IsStarted = false; } } public Task Save(VideoConfiguration config, String outputFile) { if (IsStarted) { throw new InvalidOperationException("Stop before save!"); } return Task.Factory.StartNew(() => { try { //Wait for queue to complete. while (_framesQueue.Count > 0) { } String args = String.Empty; int width = config.FrameWidth % 2 == 0 ? config.FrameWidth : config.FrameWidth + 1; int height = config.FrameHeight % 2 == 0 ? config.FrameHeight : config.FrameHeight + 1; args = $"-y -loglevel info -framerate {Math.Round(config.FrameRate, 1)} -i \"{Path.Combine(_tempFolder, "image")}%d.png\" -f mp4 -c:v libx264 -crf {(int)config.Quality} -vf scale={width}:{height} \"{outputFile}\""; var startInfo = new ProcessStartInfo(_ffmpegPath, args); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardInput = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; Regex progressRegEx = new Regex("frame= (\\d+) "); var process = Process.Start(startInfo); process.ErrorDataReceived += (x, e) => { if (e.Data != null) { var match = progressRegEx.Match(e.Data); if (match.Success && match.Groups.Count == 2) { int frame = int.Parse(match.Groups[1].Value); OnProgress(frame, _totalFrames); } } }; process.OutputDataReceived += (x, e) => { }; process.BeginOutputReadLine(); process.BeginErrorReadLine(); process.WaitForExit(); } catch (Exception ex) { throw ex; } finally { ClearTemporaryFiles(); } }); } public void PushFrame(BitmapSource source) { if (IsStarted) { var cloned = source.Clone(); cloned.Freeze(); _framesQueue.BlockEnqueue(new QueueItem() { Frame = cloned }); } } private void ClearTemporaryFiles() { _tempFolder?.DeleteAsync(); } protected virtual void OnProgress(int frame, int totalFrames) { Progress?.Invoke(this, new TangoProgressChangedEventArgs() { Progress = new TangoProgress() { IsIndeterminate = false, Maximum = totalFrames, Value = frame, }, }); } public void Dispose() { Stop(); ClearTemporaryFiles(); } } }