Xwork's Blog

The lord is waiting to take your hand.

Mostrando las entradas con la etiqueta Vencoder. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Vencoder. Mostrar todas las entradas

El Video Correcto #10 - VEncoder y subtitulos.

Hola de nuevo a todos, el día de hoy vamos a ver cómo insertar subtitulos en los videos que se codifiquen con VEncoder.


Hay que recordar que VEncoder es una interfaz de usuario para FFMPEG; si bien la idea es mantener los mas simple posible todo, para lograr resultados complejos, es necesario hacer ajustes complejos. Antes de continuar, me gustaría recapitular cual fue la idea de VEncoder. El programa inicialmente nació cómo una herramienta para convertir los videos para que el Play Station Portable pudiera reproducirlos, incluso era parte de un programa llamado PSP Manager, y cómo su nombre lo pudiera sugerir, servía para manejar el contenido multimedia de la consola. Poco a poco cada una de las partes se separó y dieron origen a VEncoder y AEncoder que a diferencia de PSP Manager, no tuvieron tanta relevancia.


He usado mi PSP desde el 2010 y de las pocas cosas que no he logrado entender, es cómo funciona la opción de los subtítulos en el reproductor de video.


Recientemente me he encontrado con gente para la cual la consola es algo totalmente nuevo y he compartido algunos videos con esas personas, pero cómo los tiempos cambian, ahora resulta más sencillo encontrar contenido que no está en nuestro idioma por lo que resulta necesario encontrar una forma de incluirlos. Cómo mencioné hace poco, a pesar de que la PSP tiene (o eso según el menú) la capacidad de reproducir subtítulos, nunca supe cómo usarlos. 

FFMPEG tiene la capacidad de reproducir subtítulos, pero antes de detallar cómo es que funcionan, hay que resaltar algo importante. Hay dos formas de incrustar los subtítulos; la primera es sencilla, se dota de un archivo que tiene la transcripción de los diálogos con los tiempos para cada uno, el reproductor los lee y los muestra sobre el video, a esto le podemos llamar "subtítulos por software". La otra forma es similar, tenemos un archivo con las transcripciones pero solo las usamos al momento de codificar el video y justo en la codificación, vamos a modificar la imagen correspondiente al tiempo del fotograma, de tal forma que el texto quede cómo parte de la misma, con eso bastará para que los subtítulos queden de forma permanente en el video.

Para lograr esto, usaremos FFMPEG junto con VEncoder (el cual actualizare pronto para que la opción "Si hay subtítulos 'quemarlos'" funcione). En VEncoder, hay una ficha llamada "Parámetros adicionales" dentro, hay una ficha llamada "video". FFMPEG soporta varios tipos de subtítulos, archivos SRT o archivos ASS, los primeros son mas sencillos, pues solo incluyen el dialogo y el tiempo en que deben de aparecer; los segundos incluyen "estilos" y efectos (transiciones, diferentes ubicaciones para el texto).

Primero veamos un ejemplo de cómo insertar un archivo SRT.

 -vf "subtitles='I\:\\\VIDEOS\\\Track3.srt'"
En VEncoder, bastará con agregar la línea anterior a los parámetros adicionales del video, para este caso, tenemos que nuestro archivo de subtítulos se llama "Track3.srt" y que su ruta completa es "I:\VIDEOS\Track3.srt", en FFMPEG para escapar las contra-diagonales, necesitamos usar doble "\". Supongamos que tenemos la siguiente ruta "c:\VIDEOS \Dr House\Season 1\Pilot.srt", la ruta para usarla en FFMPEG quedaria: "c\:\\\VIDEOS\\\Dr House\\\Season 1\\\Pilot.srt".

Si usamos archivos ASS, la sintaxis es similar.

-vf "ass='I\:\\\VIDEOS\\\Track3.srt'"

La codificación para los archivos ass tardará mas tiempo, dependiendo de los estilos y los efectos, por lo que en la medida de lo posible recomiendo usar archivos SRT.

No es precisamente el mismo cuadro, pero se ven lo subtítulos "quemados"

Y bien, por ahora es todo, aun quedan varias cosas que tratar para poder visualizar los videos de forma correcta en el PSP, una de las cosas más comunes, es encontrar la relación de aspecto adecuada para cada video, la resolucion y la calidad, pero eso lo veremos en el siguiente post.

Los leo luego.

Vamos a programar #85 - VEncoder 2.1

Hola de nuevo a todos, el día de hoy vamos a ver una pequeña actualizacion en el código de VEncoder, ahora pasando de la version 2.0.1 a la version 2.1.

La semana pasada vimos cómo hacer un par de "cosillas" en FFMpeg, de forma ideal, deberíamos de ser capaces de aplicarlas en VEncoder, pero aquí es donde surge el dilema. Tratar de aumentar las opciones, también aumentaría la complejidad del programa y la idea principal (y para la cual fue hecha VEncoder), fue tratar de reducir lo complejo y poder convertir los archivos usando pocos clics. Para mantener un poco el balance entre la usabilidad y la complejidad, la única opción que encontré fue abrir la posibilidad de usar parámetros adicionales. Antes de continuar, veamos el código de VENcoder.

namespace VEncoder_2
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Drawing;
	using System.IO;
	using System.Resources;
	using System.Text.RegularExpressions;
	using System.Threading;
	using System.Windows.Forms;
	using System.Xml;
	using MediaInfoNET;
	using System.Text;

	/// <summary>
	/// Formulario principal de VEncoder 2
	/// </summary>
	public partial class FrmMain : Form
	{
		
		public FrmMain()
		{
			InitializeComponent();
		}
		
		#region Declaraciones
		
		private bool IsConvertingSomething = false;
		private bool RequestForCancel = false;
		private int ElapsedTimeGlobal = 0;
		private int ElapsedTimeCurrent = 0;
		
		#endregion
		
		#region Funciones
		
		private void SaveProfile(string XmlPath, string ProfileName)
		{
			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;
			settings.IndentChars = "\t";
			using (XmlWriter XWriter = XmlWriter.Create(XmlPath, settings))
			{
				XWriter.WriteStartDocument();
				XWriter.WriteComment("Archivo de preset para VEncoder 2");
				XWriter.WriteStartElement("Perfil");
				XWriter.WriteAttributeString("v", "1");
				XWriter.WriteAttributeString("appname", "VEncoder2");
				XWriter.WriteElementString("Name", ProfileName);
				XWriter.WriteElementString("WH", CboResolucion.Text);
				XWriter.WriteElementString("VBit", TXTVBit.Text);
				XWriter.WriteElementString("Ratio", CBORatio.Text);
				XWriter.WriteElementString("Profile", CBOProfile.Text);
				XWriter.WriteElementString("Nivel", CboLevel.Text);
				XWriter.WriteElementString("ASample", CboFreq.Text);
				XWriter.WriteElementString("ABit2", CBOBitsA.Text);
				XWriter.WriteElementString("Channel", CBOAChannels.Text);
				//XWriter.WriteElementString("ExtraParams", TXTExtraparams.Text);
				XWriter.WriteEndElement();
				XWriter.WriteEndDocument();
			}
		}
		
		/// <summary>
		/// Carga un archivo XML con los ajustes para realizar la cadificacion.
		/// </summary>
		/// <param name="InXMLFile">Ruta al archivo que se debe de leer</param>

		private void LoadProfile(string InXMLFile)
		{
			using (XmlReader XMLR = new XmlTextReader(InXMLFile))
			{
				try
				{
					XMLR.ReadToFollowing("Name");
					TSSLblProfile.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("WH");
				    CboResolucion.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("VBit");
				    TXTVBit.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("Ratio");
				    CBORatio.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("Profile");
				    CBOProfile.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("Nivel");
				    CboLevel.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("ASample");
				    CboFreq.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("ABit2");
				    CBOBitsA.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("Channel");
				    CBOAChannels.Text = XMLR.ReadElementContentAsString();
				    XMLR.ReadToFollowing("ExtraParams");
				    //TXTExtraparams.Text = XMLR.ReadElementContentAsString();

				}
				catch
				{
					MessageBox.Show("Parece que el XML no es valido", "VEncoder 2.0.1", MessageBoxButtons.OK, MessageBoxIcon.Error);
				}

			}
		}
		
		/// <summary>
		/// Funcion para arreglar la duracion de un archivo
		/// </summary>
		/// <param name="Duration">La duracion en segundos</param>
		/// <returns>Regresa un entero con la duracion total del archivo</returns>
		/// <remarks>Debido a que mediainfo.dll no es precisa con la duracion; es el metodo facil</remarks>
		
		private int FixTimeDuration(int Duration)
		{
			if (Duration <= 3600)
			{
				return Duration + 1;
			}else{
				return Duration + 60;
			}
		}
		
		/// <summary>
		/// Calcula cuanto tiempo flata para terminar un proceso.
		/// </summary>
		/// <param name="Porciento">Porcentaje actual completo</param>
		/// <param name="TransTime">Tiempo trasncurrido</param>
		/// <returns>Regresa lel tiempo restante calculado con los datos proporcionados</returns>
		
		private Single TiempoRestante(Single Porciento, int TransTime)
		{
			Single Z;
			//z = tiempo gastado por cada 1%
			if (Porciento == 0)
			{
				Porciento = 1;
			}
			Z = TransTime / Porciento;
			return (100 - Porciento) * Z;
		}
		
		/// <summary>
		/// Calcula porcentaje
		/// </summary>
		/// <param name="ValorActual">Valor actual para hacer el calculo</param>
		/// <param name="ValorTotal">Valor total para hacer el calculo</param>
		/// <returns>Un numero que representa el valor del porcentaje</returns>
		
		private Single Porcentaje(Single ValorActual, Single ValorTotal)
		{
			return (ValorActual / ValorTotal) * 100;
		}
		
		/// <summary>
		/// Obtiene el nombre de archivo desde una ruta completa
		/// </summary>
		/// <param name="FileName">La ruta de la cual se quiere obtener el nombre de archivo</param>
		/// <returns>Un string con el equivalente al nombre de archivo</returns>
		/// <remarks>El valor devuelto no incluye la extension</remarks>
		
		private string GetFileName(string FileName)
		{
			string[] TMPData = FileName.Split((char)92);
			return TMPData[TMPData.Length - 1];
		}
		
		/// <summary>
		/// Obtiene el valor de tiempo desde una linea de resultado de ffmpeg
		/// </summary>
		/// <param name="FFText">Linea proveniente de FFMPEG</param>
		/// <returns>Regresa un numero con el valor en segundos</returns>
		
		private Single GetPositionFromFFline(string FFText)
		{
			// (\d+)\.\d+x find speed
			try
			{
				if (FFText.StartsWith("FRAME", StringComparison.OrdinalIgnoreCase)) {
					Single CurrentValue = 0;
					MatchCollection wordColl = Regex.Matches(FFText, @"(\d+):(\d+):(\d+)?(.\d+)");			
					CurrentValue = (Single)TimeSpan.Parse(wordColl[0].ToString()).TotalSeconds;
					return CurrentValue;
				}else{
					return 1;
				}
			} catch (Exception) {
				return 1;
			}
		}

		private string GetSpeedFromFFLine(string FFText)
		{
			try
			{
				if (FFText.StartsWith("FRAME", StringComparison.OrdinalIgnoreCase))
				{
					string CurrentValue = "0x";
					MatchCollection wordColl = Regex.Matches(FFText, @"((\d+)(\.\d+)?)x");
					CurrentValue = wordColl[0].ToString();
					return CurrentValue;
				}
				else
				{
					return "???x";
				}
			}
			catch (Exception)
			{
				return "!!!x";
			}
		}

		/// <summary>
		/// Prepara la linea de comandos y lo necesario para hacer la conversion
		/// </summary>
		/// <param name="HardWork">Parametro booleano que indica como se debe de hacer el trabajo</param>
		/// <remarks>Si HardWork es true, todos lo parametros se pasaran a la conversion; en caso contrario,
		/// solo se genera la linea de comandos para usarse de forma externa con FFMPEG</remarks>
		
		private void PrepareWork(bool HardWork = false)
		{
			List<string> Lote = new List<string>();
			List<int> DLote = new List<int>();
			if (CHKAll.Checked == true)
			{
				foreach (ListViewItem LV in lvfilesin.Items)
				{
					if (LV.Checked)
					{
						if (CHK2pass.Checked == true)
						{
							Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
													  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
													  CBOAChannels.Text, TXTSavePath.Text,TXTExtraParamsVideo.Text,TxtExtraParamsAudio.Text,TxtExtraParamsGen.Text, true, false, false));
							DLote.Add(FixTimeDuration((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds));
							Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
													  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
													  CBOAChannels.Text, TXTSavePath.Text, TXTExtraParamsVideo.Text, TxtExtraParamsAudio.Text, TxtExtraParamsGen.Text, false, true, false));
							DLote.Add((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds + 1);
						}else{
							Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
													  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
													  CBOAChannels.Text, TXTSavePath.Text, TXTExtraParamsVideo.Text, TxtExtraParamsAudio.Text, TxtExtraParamsGen.Text, false, false, true));
							DLote.Add(FixTimeDuration((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds));
						}
					}
				}
			}else{
				foreach (ListViewItem LV in lvfilesin.Items)
				{
					if (CHK2pass.Checked == true)
					{
						Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
												  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
												  CBOAChannels.Text, TXTSavePath.Text, TXTExtraParamsVideo.Text, TxtExtraParamsAudio.Text, TxtExtraParamsGen.Text, true, false, false));
						DLote.Add(FixTimeDuration((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds));
						Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
												  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
												  CBOAChannels.Text, TXTSavePath.Text, TXTExtraParamsVideo.Text, TxtExtraParamsAudio.Text, TxtExtraParamsGen.Text, false, true, false));
						DLote.Add(FixTimeDuration((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds));
					}else{
						Lote.Add(BuildCommandLine(LV.Text, CBOCodec.Text, CboResolucion.Text,
												  TXTVBit.Text, CBORatio.Text, CBOProfile.Text, CboLevel.Text, CboFPS.Text, CBOCodecA.Text, CBOBitsA.Text, CboFreq.Text,
												  CBOAChannels.Text, TXTSavePath.Text, TXTExtraParamsVideo.Text, TxtExtraParamsAudio.Text, TxtExtraParamsGen.Text, false, false, true));
						DLote.Add(FixTimeDuration((int)TimeSpan.Parse(LV.SubItems[3].Text).TotalSeconds));
					}
				}
			}
			if (HardWork)
			{
				TimerElapsed.Enabled = true;
				DoWork(Lote, DLote);
			}else{
				foreach (String LineCom in Lote)
				{
					RTBCommandLine.Text = RTBCommandLine.Text + LineCom + "\n";
				}
			}
		}

		private string BuildExtraCommands(string ExtraParams)
		{
			return  " " + ExtraParams;
		}

		private string BuildAudioCommand( string AudioCodec, string AudioBitRate = "", string AudioFreq = "", string Channels = "", string ExtraParams = "")
		{
			if (string.Compare(AudioCodec,"copy",true) == 0){
				return " -c:a copy ";
			}else{
				string TempCommand = " -c:a " + AudioCodec;
				if (string.Compare(AudioBitRate, "copy", true) == -1)
					TempCommand += " -b:a " + AudioBitRate + "k";

				if (string.Compare(AudioFreq, "copy", true) == -1)
					TempCommand += " -ar " + AudioFreq ;

				if (string.Compare(Channels, "copy", true) == -1)
					TempCommand += " -ac " + Channels;

				TempCommand +=  " " + ExtraParams;

				return TempCommand;
			}

		}
		private string BuildVideoCommand(string VCodec,string VSize = "", string VBit = "", string VAspect = "",string VProfile = "", string VLevel = "", string VFPS = "",
			string ExtraParams = "")
		{
			if (string.Compare(VCodec, "copy", true) == 0)
			{
				return " -c:v copy";
			}
			else
			{
				string TempCommand = " -c:v " + VCodec;
				if (string.Compare(VSize, "copy", true) == -1)
					TempCommand += " -s " + VSize;

				if (string.Compare(VBit, "copy", true) == -1)
					TempCommand += " -b:v " + VBit +"k";

				if (string.Compare(VAspect, "copy", true) == -1)
					TempCommand += " -aspect " + VAspect;

				if (string.Compare(VProfile, "copy", true) == -1)
					TempCommand += " -profile:v " + VProfile;

				if (string.Compare(VLevel, "copy",true) == -1)
					TempCommand += " -level " + VLevel;

				if (string.Compare(VFPS, "copy", true) == -1)
					TempCommand += " -r " + VFPS;

				TempCommand += " " + ExtraParams;

				return TempCommand;
			}
		}


		private string BuildCommandLine(string InFile,string VCodec,string VideoSize,string VideoBit,string VAspect,string VProfile, string VLevel,string FrameRate, string ACodec ,string AudioBit,
		                                string AudioFreq, string AChannels,string OutPath , string VidExtraParams, string AudExtraParams, string  GenExtraParams,bool ForFirstPass= false,bool AfterPass1=false,bool SinglePass= true)
		{
			if (ForFirstPass){
				return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate, VidExtraParams), " -pass 1 -an ", GenExtraParams, " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
			}
			if(AfterPass1){
				return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate, VidExtraParams), " -f mp4 -pass 2 ", GenExtraParams ,BuildAudioCommand(ACodec, AudioBit, AudioFreq, AChannels, AudExtraParams), " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
			}
			if (SinglePass) {
				return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate, VidExtraParams), " -f mp4 ", GenExtraParams , BuildAudioCommand(ACodec, AudioBit, AudioFreq, AChannels, AudExtraParams), " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
			}else{
				return "Test";
			}
		}
		
		/// <summary>
		/// Inicia la conversion de los archivos
		/// </summary>
		/// <param name="Params">Una lista del tipo string que contiene las lineas de comandos a usarse</param>
		/// <param name="Durations">Una lista del tipo int que contiene las duraciones de los archivos</param>
		
		private void DoWork(List<string>  Params, List<int> Durations)
		{
			Thread DoConversion = new Thread(new ThreadStart(() => {
			                                                 	int CD = 0;
			                                                 	foreach (string Param in Params)
			                                                 	{
			                                                 		if (RequestForCancel == true)
			                                                 			break;
			                                                 		
			                                                 		int TotalPercentConvert = CD * 100;
			                                                 		int CurrentDuration = Durations[CD];
			                                                 		Process Proc = new Process();
			                                                 		StreamReader FFResult;
			                                                 		string CurrentResult;
			                                                 		ProcessStartInfo FFProcess = new ProcessStartInfo("ffmpeg.exe");
			                                                 		FFProcess.Arguments = Param;
																	FFProcess.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
			                                                 		FFProcess.UseShellExecute = false;
			                                                 		FFProcess.WindowStyle = ProcessWindowStyle.Hidden;
			                                                 		FFProcess.RedirectStandardError = true;
			                                                 		FFProcess.RedirectStandardOutput = true;
			                                                 		FFProcess.CreateNoWindow = true;
																	
			                                                 		Proc.StartInfo = FFProcess;
			                                                 		Proc.Start();
			                                                 		PBProgress.BeginInvoke(new Action(() => PBProgress.Maximum = CurrentDuration));
			                                                 		PBGlobalProgress.BeginInvoke(new Action(() => PBGlobalProgress.Maximum = Durations.Count * 100));
			                                                 		FFResult = Proc.StandardError;
			                                                 		do
			                                                 		{
			                                                 			if (RequestForCancel == true)
			                                                 			{
			                                                 				Proc.Kill();
			                                                 				break;
			                                                 			}
			                                                 			
			                                                 			CurrentResult = FFResult.ReadLine();
			                                                 			Debug.Print(CurrentResult);
			                                                 			this.BeginInvoke(new Action(() => {
			                                                 			                            	//this.lblProgressInfo.Text = TimeSpan.FromSeconds(GetPositionFromFFline(CurrentResult)).ToString();
																										this.lblProgressInfo.Text = GetSpeedFromFFLine(CurrentResult);
			                                                 			                            	this.PBProgress.Value = (int)GetPositionFromFFline(CurrentResult);
			                                                 			                            	this.PBGlobalProgress.Value = TotalPercentConvert + (int)Porcentaje(GetPositionFromFFline(CurrentResult),CurrentDuration);
			                                                 			                            	TaskbarProgress.SetValue(this.Handle, this.PBGlobalProgress.Value, this.PBGlobalProgress.Maximum);
			                                                 			                            }));
			                                                 		}while (Proc.HasExited == false & string.Compare(CurrentResult, "") == 1);
			                                                 		CD = CD + 1;
			                                                 		ElapsedTimeCurrent = 0;
			                                                 	}
			                                                 	IsConvertingSomething = false;
			                                                 	RequestForCancel = false;
			                                                 	ElapsedTimeCurrent = 0;
			                                                 	ElapsedTimeGlobal = 0;
			                                                 	this.BeginInvoke(new Action(() => {
			                                                 	                            	this.lblProgressInfo.Text = "Esperando";
			                                                 	                            	this.PBProgress.Value = 0;
			                                                 	                            	this.PBGlobalProgress.Value = 0;
			                                                 	                            	TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.NoProgress);
			                                                 	                            }));
			                                                 }));
			DoConversion.Start();
		}
		
		/// <summary>
		/// Obtiene la informacion de los archivos de entrada
		/// </summary>
		/// <param name="Files">Archivos de los que se obtendra la informacion.</param>
		
		private void SetInfo(List<string> Files)
		{
			Thread GetInfo = new Thread(new ThreadStart(() => {
			                                            	foreach (string Archivo in Files)
			                                            	{
			                                            		ListViewItem InfoItem = new ListViewItem(Archivo);
			                                            		MediaFile mFile = new MediaFile(Archivo);
			                                            		InfoItem.SubItems.AddRange(new string[] { mFile.Video[0].FrameSize.ToString(),mFile.Video[0].FrameRate.ToString(),
			                                            		                           	mFile.Video[0].DurationString,mFile.Video[0].Bitrate.ToString(),mFile.Audio.Count.ToString(),
			                                            		                           	mFile.Audio[0].SamplingRate.ToString(),mFile.Audio[0].Bitrate.ToString(),mFile.Audio[0].Channels.ToString()});
			                                            		lvfilesin.BeginInvoke(new Action(() => lvfilesin.Items.Add(InfoItem)));
			                                            	}
			                                            	this.BeginInvoke(new Action(() => TaskbarProgress.SetState(this.Handle,TaskbarProgress.TaskbarStates.NoProgress)));
			                                            }));
			GetInfo.Start();
		}
		
		#endregion
		void FrmMainLoad(object sender, EventArgs e)
		{
			CBOCodec.SelectedIndex = 0;
			CboResolucion.SelectedIndex = 3;
			CBORatio.SelectedIndex = 0;
			CBOBitsA.SelectedIndex = 4;
			CBOProfile.SelectedIndex = 1;
			CBOAChannels.SelectedIndex = 1;
			CBOCodecA.SelectedIndex = 1;
			if (Environment.Is64BitOperatingSystem)
				TSLblInfo.Text = "64 Bits";
			else
				TSLblInfo.Text = "32 Bits";
			if (System.IO.File.Exists("ffmpeg.exe") == false)
			{
				switch (MessageBox.Show("No se puede encontrar FFMPEG.exe\n¿Quieres buscarlo?", "VEncoder 2.0", MessageBoxButtons.YesNo
				                        , MessageBoxIcon.Error))
				{
					case DialogResult.Yes:
						using (OpenFileDialog OpDiag = new OpenFileDialog())
						{
							OpDiag.Title = "Buscar FFMPEG " + TSLblInfo.Text;
							OpDiag.Filter = "FFMpeg|FFmpeg.exe|ffmpeg32|ffmpeg32.exe|ffmpeg64|ffmpeg64.exe|Aplicaciones|*.exe";
							DialogResult DR = OpDiag.ShowDialog();
							if (DR == DialogResult.OK)
							{
								System.IO.File.Copy(OpDiag.FileName, "FFMPEG.EXE", true);
							}
						}
						break;
					case DialogResult.No:
						BtnDo.Enabled = false;
						break;
					default:
						throw new Exception("Invalid value for DialogResult");
				}
			}
		}

		private void TSMIopen_Click(object sender, EventArgs e)
		{
			OpenFileDialog OpDiag = new OpenFileDialog();
			OpDiag.Multiselect = true;
			OpDiag.Filter = "Todos los archivos.|*.*";
			OpDiag.ShowDialog(this);
			if (OpDiag.FileNames.Length > 0)
			{
				List<string> Archivos = new List<string>(OpDiag.FileNames);
				TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Indeterminate);
				SetInfo(Archivos);
			}
		}
		
		void BtnFolderSelClick(object sender, EventArgs e)
		{
			FolderBrowserDialog FolderSelect = new FolderBrowserDialog();
			FolderSelect.ShowNewFolderButton = true;
			DialogResult DR = FolderSelect.ShowDialog(this);
			if (DR == DialogResult.OK)
			{
				TXTSavePath.Text = FolderSelect.SelectedPath;
			}
		}
		
		void BtnDoClick(object sender, EventArgs e)
		{
			if (lvfilesin.Items.Count < 1)
			{
				MessageBox.Show("No hay archivos", "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				return;
			}
			if (TXTSavePath.Text.Length < 3)
			{
				BtnFolderSelClick(null, null);
				return;
			}
			TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Normal);
			PrepareWork(true);
			IsConvertingSomething = true;
			BtnDo.Enabled = false;
		}
		
		void BtnRemoveClick(object sender, EventArgs e)
		{
			lvfilesin.Items.Clear();
		}
		
		void BtnRemoveCheckClick(object sender, EventArgs e)
		{
			foreach (ListViewItem LV in lvfilesin.CheckedItems)
			{
				lvfilesin.Items.Remove(LV);
			}
		}
		
		void BtnCheckAllClick(object sender, EventArgs e)
		{
			for (int i = 0; i < lvfilesin.Items.Count; i++)
			{
				lvfilesin.Items[i].Checked = true;
			}
		}
		
		void BtnInverSelClick(object sender, EventArgs e)
		{
			for (int i = 0; i < lvfilesin.Items.Count; i++)
			{
				lvfilesin.Items[i].Checked =! lvfilesin.Items[i].Checked;
			}
		}
		
		void FrmMainFormClosing(object sender, FormClosingEventArgs e)
		{
			if (IsConvertingSomething == true)
			{
				switch (MessageBox.Show("Hay un trabajo en progreso. ¿Realmente quieres salir?",
				                        "Pregunta", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation)) {
					case DialogResult.None:
						e.Cancel = true;
						break;
					case DialogResult.Yes:
						break;
					case DialogResult.No:
						e.Cancel = true;
						break;
					default:
						throw new Exception("Invalid value for DialogResult");
				}
			}
		}
		
		void TimerElapsedTick(object sender, EventArgs e)
		{
			ElapsedTimeGlobal = ElapsedTimeGlobal + 1;
			ElapsedTimeCurrent = ElapsedTimeCurrent + 1;
			TSLBLElapsedTime.Text = "Transcurrido: " + TimeSpan.FromSeconds((double)ElapsedTimeGlobal).ToString();
			lblPercentProgress.Text = Porcentaje((float)PBProgress.Value, (float)PBProgress.Maximum).ToString() + "%";
			TSLBLRemainAc.Text = "Tiempo restante: " + TimeSpan.FromSeconds(
				(int)TiempoRestante(Porcentaje((float)PBProgress.Value, (float)PBProgress.Maximum),
				                    ElapsedTimeCurrent)).ToString();
			if (IsConvertingSomething == false)
			{
				TimerElapsed.Enabled = false;
				BtnDo.Enabled = true;
				TSLBLElapsedTime.Text = "En espera";
				TSLBLRemainAc.Text = "En Espera";
			}
		}
		
		void BtnCancelClick(object sender, EventArgs e)
		{

			if (IsConvertingSomething == true)
			{
				switch (MessageBox.Show("Quieres cancelar el trabajo actual",
				                        "Cancelar?",
				                        MessageBoxButtons.YesNo,
				                        MessageBoxIcon.Exclamation))
				{
					case DialogResult.Yes:
						RequestForCancel = true;
						break;
					case DialogResult.None:
					case DialogResult.No:
						break;
					default:
						throw new Exception("Invalid value for DialogResult");
				}
			}
		}
		
		void TSMIMakeCommandClick(object sender, EventArgs e)
		{
			if (lvfilesin.Items.Count < 1)
			{
				MessageBox.Show("No hay archivos", "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				return;
			}
			if (TXTSavePath.Text.Length < 3)
			{
				BtnFolderSelClick(null, null);
				return;
			}
			RTBCommandLine.Clear();
			PrepareWork(false);
		}
		
		void TSMIExitClick(object sender, EventArgs e)
		{
			Application.Exit();
		}
		
		void LnklblLoadProfileLinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
		{

		}
		
		void TSMISaveProfileClick(object sender, EventArgs e)
		{
			using (SaveFileDialog SavDiag = new SaveFileDialog())
			{
				SavDiag.Title = "Guardar perfil";
				SavDiag.Filter = "Perfil de VEncoder|*.vepx|Archivo XML|*.xml";
				if (SavDiag.ShowDialog() == DialogResult.OK)
				    SaveProfile(SavDiag.FileName, "Test0005");
			}
		}

		private void TSSLblProfile_Click(object sender, EventArgs e)
		{
			using (OpenFileDialog OpDiag = new OpenFileDialog())
			{
				OpDiag.Title = "Cargar perfil";
				OpDiag.Filter = "Archivo de perfil VEncoder|*.vepx|XML|*.XML;*.vepx";
				if (OpDiag.ShowDialog() == DialogResult.OK)
					LoadProfile(OpDiag.FileName);

			}
		}
	}
}


Las funciones que han cambiado son "BuildVideoCommand()" y "BuildAudioCommand()", además se ha agregado la función "BuildExtraCommands(). Las primeras dos funciones ya estaban hechas y solo se actualizaron un poco.

Pero antes, cual es la idea? si el chiste era crear una interfaz para que el usuario no tuviera que escribir nada; y así es, la idea de usar los parámetros extra, es que los usuarios que requieran hacer algo que no está programado en VEncoder, puedan hacerlo y aquellos que requieran algo "más", también puedan hacerlo. Si no se usa ningún parámetro adicional, el programa funcionara cómo venia haciéndolo, pero el hecho de poder "extender" la usabilidad, abre la opción a que la gente experimente. Una vez dicho lo anterior, continuemos con la explicacion.

Las funciones "BuildAudioCommand()" y "BuildVideoCommand()"; ambas recibían parámetros del tipo "string" y su objetivo era construir la linea de comandos. Para hacer uso de los parámetros adicionales, simplemente agregaremos al final de los parámetros que ya teníamos y las funciones devuelven la linea de comandos más los parámetros adicionales si es que se agregan.

La función "BuildExtraCommands()" simplemente toma el texto de una caja de texto y lo agrega al final de la linea de comandos (al igual que las anteriores, si está vacía no pasa nada).

Para agregar los parámetros adicionales, ahora se ha agregado una nueva pestaña llamada "Parámetros Adicionales", y tiene un conjunto de subpestañas en la cual es posible agregar los parámetros.

Tomemos cómo ejemplo las siguiente linea de comandos:

ffmpeg.exe -y -i "X:\VIDEOS\Pokemon Pelicula 20.mkv" -c:v libx264 -s 480x272 -b:v 1200k -aspect 16:9 -level 31 -f mp4 -threads 0 -c:a aac -b:a 128k -ar 44100 -ac 2  "X:\Dropbox\Pokemon Pelicula 20.mkv.mp4"

Ahora, si agregamos en el apartado de Video de los parametros adicionales "-tune animation" y en el apartado general "-t 600 -ss 140", la linea de comados resultante será la siguiente:

ffmpeg.exe -y -i "X:\VIDEOS\Pokemon Pelicula 20.mkv" -c:v libx264 -s 480x272 -b:v 1200k -aspect 16:9 -level 31 -tune animation -f mp4 -threads 0 -t 600 -ss 140 -c:a aac -b:a 128k -ar 44100 -ac 2  "X:\Dropbox\Pokemon Pelicula 20.mkv.mp4"

Cómo podras observar, ahora funcionan otras funciones, si quieres saber cuales están disponibles para el formato mp4 usando el codificador X264, puesde ver la guia de la buena codificacion (en inglés).

A partir de ahora podemos hacer uso extensivo de VEncoder, pero eso trae un par de detalles que aun se deben de resolver, pero el problema es el mismo, cómo aumentar la funcionalidad sin aumentar la complejidad. Cómo de costumbre el código lo puedes descargar de mi dropbox para que lo uses y lo modifiques. Aun queda trabajo pendiente, pero por ahora es todo.

Los leo luego.

Vamos a programar #84 - Camino hacía VEncoder 2.1 - Parametros para FFMpeg.

Hola de nuevo a todos, el día de hoy vamos ver el código actualizado de VEncoder, ahora pasando de la versión 2.00.1 a la versión 2.1; por lo que es una actualización menor pero un poco significativa.


Pero antes que nada, vamos a ver un poco del trasfondo para todo esto. Cómo de costumbre mientras veia los comentarios en twitter, hubo mucha gente que decia que al realizar la conversión de un archivo, muchas veces el resultado no era compatible, o se veia raro o el audio perdía sincronía. Una de las principales razones para crear VEncoder fue mantener las cosas simples. Hace no mucho, bastaba con proporcionar los parámetros que se muestran en la interfaz, de ahi, ffmpeg realizaba la conversión y el resultado era compatible en el 90 de los casos. Pero cómo las cosas avanzan, la complejidad del programa aumento y es capaz de hacer muchas cosas y muy complejas. Cómo es dificil cubrir todas las posibilidades (al menos sin aumentar la complejidad al usuario), primero veremos cómo es que se resuelven algunos de los problemas mencionados usando la linea de comandos y después (en otro post); veremos cómo es que se pueden aplicar las soluciones en VEncoder.

Algunas de las situaciones que me han llegado en los comentarios, es que muchas veces tratan de usar una pista de audio externa, o incluso que dentro del mismo video existen dos o tres pistas de audio (generalmente una por cada idioma) y que al realiza la conversión con la aplicación; por default, siempre tomará la primera de ellas y no necesariamente será la deseada.
Para poder usar otra pista de audio, si la tenemos de forma externa (en otro archivo), podremos usar una línea de comandos cómo la que sigue:

ffmpeg.exe -y -i "X:\DOCS\CAMERA\videoplayback.mp4" -i "X:\DOCS\CAMERA\test.m4a" -c:v copy -threads 0 -f mp4 -c:a copy "x:\Videos\tetst.mp4"

Con ello indicamos que vamos a “ingresar”  dos archivos y que la pista de audio; en lugar de usar la que está contenida en el video (si es que la tiene);  deberá de usar la que usamos cómo segundo archivo de entrada (por lo general archivos mp3, aac, flac, etc.).

Pero al usar pistas de diferentes fuentes, lo más seguro es que no estén adecuadamente sincronizadas, por lo que será necesario hacer los ajustes para que coincidan los tiempos.
Supongamos que tenemos una situación en donde primero se escucha la voz y después se ve el movimiento de la boca, eso quiere decir que el audio está adelantado. Para solucionarlo, podemos usar un línea de comandos cómo la que sigue:

ffmpeg.exe -y -itsoffset -0.50 -i "X:\DOCS\CAMERA\videoplayback.mp4" -i "X:\DOCS\CAMERA\AV.mp4" -c:v copy -threads 0 -f mp4 -c:a copy -map 0:1 -map 1:0 "x:\Videos\TEST.mp4"


Con eso haremos que el audio empiece un poco después. Ahora, si se da el caso contrario, basta con usar el tiempo invertido, algo cómo lo que sigue:

ffmpeg.exe -y -itsoffset 0.50 -i "X:\DOCS\CAMERA\videoplayback.mp4" -i "X:\DOCS\CAMERA\AV.mp4" -c:v copy -threads 0 -f mp4 -c:a copy -map 0:1 -map 1:0 "x:\Videos\TEST.mp4"

Otro de los errores más comunes es que al querer hacer una conversión, el video resultante dice que el perfil que se usa es “High 10@L5.1”. Hace algún tiempo se empezó a emplear el perfil alto de 10bits, y FFMpeg tenia una versión en donde el codificador x264 solamente tenia la capacidad de hacer la conversión en 10 bits (por ahora solo lo diremos de está forma para no entrar en detalles). Y una versión diferente para la conversión de 8 bits.
El problema surge cuando hacemos la conversión de un video que viene codificado usando 10 bits, al intentar hacer la conversión el codificador nos mostrará un error diciendo: “x264 [error]: high profile doesn't support a bit depth of 10

Para solucionarlo, podemos usar una línea de comandos cómo la que sigue:

ffmpeg -y -i "X:\VIDEOS\Pokemon Pelicula 20.mkv"  -s 1280x720 -c:v libx264 -b:v 2048k -pix_fmt yuv420p -aspect 16:9 -profile:v high -level 4.1 -crf 10 -threads 0  -c:a copy  "X:\\Pokemon Pelicula 20.mkv"

Con ella ahora la salida de video será de 8Bits en lugar de 10.
Si es el caso contrario y queremos usar la codificación de 10 Bits, podemos usar una línea de comandos cómo la que sigue:

ffmpeg -y -i "X:\VIDEOS\Pokemon Pelicula 20.mkv"  -s 1280x720 -c:v libx264 -b:v 2048k -pix_fmt yuv420p10le -aspect 16:9 -profile:v high -level 4.1 -crf 10 -threads 0  -c:a copy  "X:\\Pokemon Pelicula 20.mkv"


Que pasa si queremos convertir solamente una fracción de nuestro video, en ffmpeg podemos indicar que solamente queremos convertir una parte de un archivo, para poder hacerlo, hay opciones. La primera de ellas es asignar cuanto tiempo es lo que queremos convertir, el parámetro “-t” sirve para indicar cuanto tiempo queremos convertir, y se le puede asignar el tiempo en formato HH:MM:SS.CC o en segundos usando el formato SS.CC, por lo que si queremos convertir un video y que su duración sea de solamente nueve minutos y medio podemos pasar el tiempo cómo: “00:09:30.00” o “570.00”.
Si queremos que la conversión no empiece desde el inicio y en su lugar queremos que empiece en una posición arbitraria, debemos de usar el parámetro “-ss” y al igual que el parámetro anterior, le podemos indicar el tiempo donde queremos que empiece la conversión usando cualquiera de los formatos de tiempo. Al realizar la conversión, se empezará de la posición dada, hasta el final del archivo.
Si queremos que la conversión se haga desde una posición determinada, pero que NO sea hasta el final de nuestro archivo, bastará con usar los dos parámetros anteriores indicando la posición de inicio (con -ss) e indicando cuanto tiempo queremos que dure (con el parámetro -t).
Para lograr una conversión optima, FFmpeg ofrece muchas opciones, pero aquí es donde la cosa se empieza a complicar, ya que al  aplicar algunos parámetros, afectaran a otros directamente, entonces siempre es importante probar cuales producen el resultado más óptimo para nuestros archivos.
Y  bien, por ahora es todo, estos fueron los problemas mas reportados por los usuarios a mi cuenta de twitter @mpm88g (no es necesario que me sigas para poder escribirme tus dudas). En el siguiente post, veremos cómo implementar todos estos parámetro en VEncoder.

Los leo luego

Vamos a programar #77 - VEncoder 2.0.1

Hola de nuevo a todos, el día de hoy vamos a ver un poco mas sobre VEncoder en su versión 2.0.1.

Hace algún tiempo que tenía ganas de actualizar la aplicación, pero por una u otra cosa, no podía hacerlo, pero hace no mucho, conseguí un PS3 y cómo es costumbre, al intentar reproducir un vídeo de los que ya tengo guardados, la consola simplemente se rehusó a hacerlo porque "el archivo de medios no es compatible".

Si hacemos un poco de memoria, el pretexto para empezar con las expresiones regulares fue precisamente VEncoder 2, pero además, en uno de los muchos post dedicados al programa, alguien mencionó que no podía hacer la conversión de sus archivos.

¿2017 fue hace un año, verdad?

Para solucionar ese problema, decidí actualizar un poco la aplicación a la versión 2.1, no son muchos los cambios, pero ahora  por lo menos serás capaz de realizar la conversión de archivos e incluiré un par de archivos con las configuraciones compatibles para el PS3.

Ahora veamos un poco del código que cambió.

private void LoadProfile(string InXMLFile)
{
	using (XmlReader XMLR = new XmlTextReader(InXMLFile))
	{
		try
		{
			XMLR.ReadToFollowing("Name");
			TSSLblProfile.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("WH");
			CboResolucion.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("VBit");
			TXTVBit.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("Ratio");
			CBORatio.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("Profile");
			CBOProfile.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("Nivel");
			CboLevel.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("ASample");
			CboFreq.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("ABit2");
			CBOBitsA.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("Channel");
			CBOAChannels.Text = XMLR.ReadElementContentAsString();
			XMLR.ReadToFollowing("ExtraParams");
			//TXTExtraparams.Text = XMLR.ReadElementContentAsString();

		}
		catch
		{
			MessageBox.Show("Parece que el XML no es valido", "VEncoder 2.0.1", MessageBoxButtons.OK, MessageBoxIcon.Error);
		}

	}
}

private Single GetPositionFromFFline(string FFText)
{
	// (\d+)\.\d+x find speed
	try
	{
		if (FFText.StartsWith("FRAME", StringComparison.OrdinalIgnoreCase)) {
			Single CurrentValue = 0;
			MatchCollection wordColl = Regex.Matches(FFText, @"(\d+):(\d+):(\d+)?(.\d+)");			
			CurrentValue = (Single)TimeSpan.Parse(wordColl[0].ToString()).TotalSeconds;
			return CurrentValue;
		}else{
			return 1;
		}
	} catch (Exception) {
		return 1;
	}
}

private string GetSpeedFromFFLine(string FFText)
{
	try
	{
		if (FFText.StartsWith("FRAME", StringComparison.OrdinalIgnoreCase))
		{
			string CurrentValue = "0x";
			MatchCollection wordColl = Regex.Matches(FFText, @"((\d+)(\.\d+)?)x");
			CurrentValue = wordColl[0].ToString();
			return CurrentValue;
		}
		else
		{
			return "???x";
		}
	}
	catch (Exception)
	{
		return "!!!x";
	}
}

private string BuildAudioCommand( string AudioCodec, string AudioBitRate = "", string AudioFreq = "", string Channels = "")
{
	if (string.Compare(AudioCodec,"copy",true) == 0){
		return " -c:a copy ";
	}else{
		string TempCommand = " -c:a " + AudioCodec;
		if (string.Compare(AudioBitRate, "copy", true) == -1)
			TempCommand += " -b:a " + AudioBitRate + "k";

		if (string.Compare(AudioFreq, "copy", true) == -1)
			TempCommand += " -ar " + AudioFreq ;

		if (string.Compare(Channels, "copy", true) == -1)
			TempCommand += " -ac " + Channels;
		return TempCommand;
	}

}
private string BuildVideoCommand(string VCodec,string VSize = "", string VBit = "", string VAspect = "",string VProfile = "", string VLevel = "", string VFPS = "")
{
	if (string.Compare(VCodec, "copy", true) == 0)
	{
		return " -c:v copy";
	}
	else
	{
		string TempCommand = " -c:v " + VCodec;
		if (string.Compare(VSize, "copy", true) == -1)
			TempCommand += " -s " + VSize;

		if (string.Compare(VBit, "copy", true) == -1)
			TempCommand += " -b:v " + VBit +"k";

		if (string.Compare(VAspect, "copy", true) == -1)
			TempCommand += " -aspect " + VAspect;

		if (string.Compare(VProfile, "copy", true) == -1)
			TempCommand += " -profile:v " + VProfile;

		if (string.Compare(VLevel, "copy", true) == -1)
			TempCommand += " -level " + VLevel;

		if (string.Compare(VFPS, "copy", true) == -1)
			TempCommand += " -r " + VFPS;

		return TempCommand;
	}
}

private string BuildCommandLine(string InFile,string VCodec,string VideoSize,string VideoBit,string VAspect,string VProfile, string VLevel,string FrameRate, string ACodec ,string AudioBit,
								string AudioFreq, string AChannels,string OutPath,bool ForFirstPass= false,bool AfterPass1=false,bool SinglePass= true)
{
	if (ForFirstPass){
		return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate), " -threads 0  -pass 1 -an ", " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
	}
	if(AfterPass1){
		return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate), " -threads 0 -f mp4 -pass 2", BuildAudioCommand(ACodec, AudioBit, AudioFreq, AChannels), " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
	}
	if (SinglePass) {
		return string.Concat("-y -i ", "\"", InFile, "\"", BuildVideoCommand(VCodec, VideoSize, VideoBit, VAspect, VProfile, VLevel, FrameRate), " -threads 0 -f mp4", BuildAudioCommand(ACodec, AudioBit, AudioFreq, AChannels), " \"", OutPath, "\\", GetFileName(InFile), ".mp4", "\"");
	}else{
		return "Test";
	}
}

El primer procedimiento en ser modificado fue "LoadProfile()", se le hizo una pequeña corrección, ya que si se cargaba un archivo XML no valido, hacia que la aplicación se detuviera, para arreglarlo, simplemente se agregaron los bloques "try" y "catch", ahora sí se carga un archivo no valido, simplemente se notifica y la aplicación continúa con normalidad.

La siguiente función en ser modificada fue "GetPositionFromFFline" aquí es donde hacemos uso de las expresiones regulares. Cuando FFMpeg está realizando una conversión, informa cual es el progreso usando una linea con aspecto similar al que sigue:
"frame=  553 fps=9.8 q=0.0 size=  160256kB time=00:00:18.53 bitrate=70848.2kbits/s dup=111 drop=0 speed=0.329x".


Y el tiempo estará formateado de la siguiente manera: "HH: MM:SS.CC", pero hay que tener en cuenta que las centésimas de segundo, solo se mostraran si son significativas, por lo que se tenemos algo cómo 00:00:22.10 se mostrará así: 00:00:22.1. Y si tenemos algo cómo 01:01:22.00 se mostrará así: 01:01:22. Tomando en cuenta las consideraciones anteriores, podemos crear una expresión regular cómo la que sigue:
(\d+):(\d+):(\d+)?(.\d+)

Con eso podremos leer el tiempo de manera un poco mas precisa.

Una nueva función que agregué es "GetSpeedFromFFLine()", cómo su nombre lo indica, sirve para leer a que velocidad se está realizando la conversión. Para encontrar el valor, hacemos uso de la siguiente expresion regular:

((\d+)(\.\d+)?)x

El patron anterior tratara de encontrar cualquier cadena que tenga la siguiente forma: D.DDx, pero al igual que el tiempo, solo se muestran los valores más significativos, por lo que si tenemos 10.10x de velocidad, FFMPeg solo devolverá 10.1x. Cuando la se excede cierta velocidad, el valor estará en notación científica, por ejemplo 1e3x, pero no lo implemente ya que no logre ver este tipo de notación cuando realizaba las conversiones (a mi tostadora con FX4100 ya le cuesta), por lo que si notas que el programa devuelve "!!!x" o "???x" muy probablemente sea a esto.

Otro de los cambios que se hicieron fue en las funciones que se usan para crear la linea de comandos, anteriormente estaba limitada (y ahora también pero no tanto) y tareas que requerían solo codificar el stream de video pero no el de audio (y viceversa) no se podían hacer. Ahora gracias a las dos nuevas funciones "BuildVideoCommand" y "BuildAudioCommand"  en adición a la función que ya teníamos "BuildCommandLine" hacen que la conversión se realice aun si no todos los parámetros son requeridos. Imaginemos que tenemos un video con el audio de buena calidad y no queremos convertirlo porque probablemente pierda calidad o no sabemos cuantos canales posee, ahora cuando encontramos una situación similar, simplemente podemos escoger o escribir "copy" en el cuadro de texto del parámetro y las funciones ignoraran ese parámetro. FFMpeg en algunos casos por default escoge los mismos valores que el origen, así si no sabemos o no queremos cambiar ese valor, ya no tendremos que preocuparnos y todo eso aplica para los parámetros que NO sean: "Velocidad de bits (Video)", "Velocidad de bits (audio)", "Codec de video" y "Codec de audio". Para el caso de la velocidad de bits de video y de audio, se tiene que asignar un valor, ya que en el caso del video, cuando no se especifica un valor, el valor es 200k (que hace parecer cualquier cosa una película hecha con bloques; por no decir una marca) y para el caso del audio es igual. Cuando se escoge "copy" en los codecs, simplemente se leerá el stream y se pasara al archivo de salida, es decir no se realizara ninguna conversion y todos los otros parámetros  serán ignorados. Esto es especialmente útil cuando tenemos archivos MKV que sabemos que son compatibles.

Y bien, por ahora es todo, en los siguientes post continuaremos con más de FFMpeg y cómo agregar mas funcionalidades a VEncoder, actualmente hay un par de errores que son vitales corregir (pero que no son tan comunes), cualquier error que encuentres siéntete libre comentarlo. Por ahora solo esta disponible el ejecutable para la descarga (cómo de costumbre desde mi dropbox), el código lo publicaré cuando soluciones los "detalles", pero bastará con agregar y reemplazar la funciones con las que se publicaron en este post al código viejo.

Los leo luego.

El Video Correcto #5 -Convertir archivos FLAC a MP3..

Hola de nuievo a todos, el día de hoy vamos a ver cómo convertir archivos FLAC (y realmente casi cualquier archivo multimedia) a MP3 usando FFMPEG.

La forma básica de hacerlo. Utilizando un bloc de notas cómo notepad++ vamos a escribir el nombre del programa; en este caso FFMPEG.exe, luego agregaremos el parámetro "-i" que sirve para indicar que queremos "ingresar" un archivo. FFMPEG acepta múltiples archivos de entrada, pero en este caso solo usaremos uno.
Luego debemos de indicar cual es el codec que vamos a usar; cómo queremos convertir a MP3, usaremos el codec que FFMPEG nos ofrece; el codec libmp3 lame.
Luego estableceremos la velocidad de bits que se usarán, para mayor calidad, usaremos el máximo disponible para MP3, que es 320Kb/s y finalmente pondremos el nombre al archivo de salida.
Suponiendo que tenemos el archivo "Infile.flac" y lo queremos convertir a "Output.mp3", podemos crear la sigueinte liena de comando:

ffmpeg.exe -i "InFile.flac" -codec:a libmp3lame -b:a 320k output.mp3
pause

Con lo anterior, podemos crear un archivo MP3 de la mejor calidad (dependiendo de la calidad del archivo FLAC mismo).


A tomar en consideración.

Hay varios factores a considerar para obtener el mejor resultado al momento de crear una versión MP3. Además de la velocidad de bits, cosas cómo la frecuencia y la cantidad de canales, afectaran el tamaño del archivo.
Ahora tomemos un ejemplo, tenemos una canción en FLAC que tiene una frecuencia de 48000Hz y queremos que nuestro archivo MP3 también tenga esa frecuencia, para indicar a FFMPEG que queremos usar esa frecuencia, la linea de comando anterior la podemos cambiar a algo cómo lo que sigue:

ffmpeg.exe -i "InFile.flac" -codec:a libmp3lame -b:a 320k -ar 48000 output.mp3
pause

Con eso se habrá creado una versión MP3 con una frecuencia de 48000 Hz, pero cómo cualquier cosa; "no podemos sacar más de lo que hay" por lo que si el archivo FLAC está a una frecuencia de 44100Hz, es un malgasto crear un MP3 a 48000Hz.

Otra de las cosas importantes, es la cantidad de canales de audio que el MP3 poseerá, normalmente todas las pistas de audio vienen en "estéreo" por lo que hay dos pistas de audio una para el canal derecho y otra para el canal izquierdo. En ocasiones solamente queremos oír todo de "un solo lado" por lo que habrá algunos que querrán convertir sus canciones en "mono", para poder cambiar los canales de audio que la canción tendrá, podemos modificar la linea anterior para  tener algo cómo lo que sigue:

ffmpeg.exe -i "InFile.flac" -codec:a libmp3lame -b:a 320k -ar 48000 -ac 1 output.mp3
pause

Al usar el parámetro "-ac" le vamos a indicar que queremos usar X número de canales de audio, para los archivos MP3, el mayor número de canales es dos. En este caso es posible crear una pista estéreo deesde una pista mono, pero al hacer esto, ambos canales de audio sonarán exactamente igual.

Para que todo lo anterior funcione, debemos de guardar el archivo de texto junto al ejecutable de FFMPEG, pero además, debemos de cambiar la extensión del archivo de "TXT" a "BAT", si no lo hacemos, al momento de abrirlo, solo mostrará su contenido en lugar de ejecutarlo. En notepad++ en el cuadro de dialogo para guardar, bastará con poner el nombre del archivo más la extensión "BAT".

Y bien, por ahora es todo, aun quedan unas cuantas cosas quedemos de saber sobre el formato MP3, pero si no te quieres complicar, puedes usar una linea de comandos cómo la primera que viste en este post, con eso FFMPEG ajustará los demás parámetros a los más indicados dependiendo del archivo FLAC que se ingrese. Probablemente haga una GUI para hacer conversiones en lotes, pero cómo aun quedan muchas cosas pendientes, a lo mejor primero actualizo VEncoder y después hago la GUI par convertir los archivos MP3.

Los leo luego.

Vamos a programar #24 - Haciendo un instalador en NSIS #3 - Datos útiles.

EL día de hoy vamos a continuar con la creación de instalador en NSIS. En post anteriores, ya hemos visto alguna cosas útiles para incluirlas en el instalador. Para continuar  y hacer un instalador lo más presentable posible, veremos algunas cosas más que ayudaran a crear un buen instalador.


Tip #2 - Apéndice de todos los valores posibles para el des-instalador.

En el post anterior vimos que hay valores que agregandolos al registro de windows, mostraran información adicional, incluirlas o no ya es cuestión de cada quien, pero yo prefiero agregar al menos las mas primordiales, esto puede ayudar a que el usuario en determinado momento opte o no por quitar nuestra aplicación.

En la siguiente tabla se muestran todas las entradas posibles y que función cumplen, además de se muestra que tipo de entrada es.

Propiedad Informacion Tipo de entrada
AuthorizedCDFPrefix La direccion URL para actualizar la aplicacion REG_SZ
Comments Comentarios adicionales para mostrarse REG_SZ
DisplayIconEl icono que se mostrara para representar a la aplicacion. REG_SZ
DisplayName El nombre del programa a mostrarse REG_SZ
DisplayVersion La version instalada del programa REG_SZ
EstimatedSize Tamaño estimado del programa  (en KB) DWORD
HelpLink Direccion URL para el soporte REG_SZ
HelpTelephone Numero telefonico para ofrecer soporte REG_SZ
InstallDate Fecha de instalacion REG_SZ
InstallLocation Ubicacion del programa instalado REG_SZ
InstallSourceUbicacion de la fuente de instalacion REG_SZ
Language Idioma del programa REG_SZ
ModifyPath La ruta del programa de re-instalacion, reparacion REG_SZ
NoModifyNO muestra la opcion de "Modificar". DWORD
NoRepairNO muestra la opcion de "Reparar"DWORD
Publisher Nombre del publicador REG_SZ
Readme La ubicacion del archivo "LEAME" REG_SZ
UninstallString La ruta del des-instalador REG_SZ
URLInfoAboutLa direccion URL con informacion del programa REG_SZ
URLUpdateInfo La direccion URL con informacion de las actualizaciones REG_SZ
Version La version del programa (X.xx.xx) REG_SZ
VersionMajor La version Major del programa (x.XX.xx) REG_SZ
VersionMinor La version Minor del programa (x.xx.XX) REG_SZ


Para agregar una entrada del tipo REG_SZ o DWORD usando NSIS usaremos las siguientes instrucciones:


WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "HelpLink" "http://xworkforall.blogspot.com/2016/07/el-video-correcto-2.html"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "NoModify" 1

La instrucción WriteRegStr es las que se usará cuando se quieran escribir valores REG_SZ (normalmente cadenas de texto) y la instrucción WriteRegDWORD, escribirá los valores DWORD (números por lo general).


Ahora supongamos que queremos agregar un comentario al instalador que usamos cómo ejemplo (antes de continuar, si no has visto el código fuente del instalador de VEncoder 2 sugiero que lo descargues y le eches un vistazo). Para agregar los comentarios, necesitamos escribir un valor en el registro, retomado el código anterior , solo hay que usar la función que escribe un valor REG_SZ y escribir un valor llamado "Comments" y su contenido será el texto que queremos mostrar.

WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "HelpLink" "http://xworkforall.blogspot.com/2016/07/el-video-correcto-2.html"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "NoModify" 1
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "Comments" "Eres libre se usarlo y modificarloa tu gusto, pero no reclamarlo cómo tuyo"

Tras re-compilar el instalador y ejecutarlo, al buscar la información en el registro lucirá cómo se ve en la siguiente imagen:

Tip #3 - Asociando una extensión a nuestro programa.

Otra parte de nuestro instalador, es la posibilidad de que ciertos tipos de archivo se abran con nuestro programa. NSIS ayuda a hacer esta tarea de forma sencilla.


Retomando el ejemplo que se incluye en la descarga, al compilarlo y ejecutarlo, verás que todos los archivos con extensión VEPX, cambian y en lugar de aparecer el icono de archivo desconocido, aparece uno como el de la imagen anterior. Si hacemos doble clic en el archivo, se abrirá un cuadro de dialogo preguntándonos que programa queremos usar para abrir ese tipo de archivos.
Normalmente, queremos que cuando el usuario haga clic en ellos, se abra nuestro programa y a partir de ahi, trabajaremos con este archivo.
Para poder abrir directamente el archivo con nuestro programa, debemos de crear entradas en el registro de Windows.

HKEY_CLASSES_ROOT
   MyProgram.exe
      shell
         open
            command
               (Default) = C:\MyDir\MyProgram.exe /a "%1"
         print
            command
               (Default) = C:\MyDir\MyProgram.exe /a /p "%1"
         printto
            command
               (Default) = C:\MyDir\MyProgram.exe /a /p "%1" "%2"

El código anterior muestra cómo se deben de crear las entradas en el registro de windows.
Para eso vamos a agregar el siguiente código en NSIS:

WriteRegStr HKCR ".VEPX" "" "VEncoder.VEPX"
WriteRegStr HKCR "VEncoder.VEPX" "" "Archivo de perfil para Vencoder"
WriteRegStr HKCR "Vencoder.VEPX\DefaultIcon" "" "$INSTDIR\VEPX.ico" 
WriteRegStr HKCR "VEncoder.VEPX\shell\Open\" "" "Cargar en VEncoder 2"
WriteRegStr HKCR "VEncoder.VEPX\shell\Open\command" "" "$INSTDIR\Vencoder 2.exe"


Lo que hace el código anterior es lo siguiente (hay que recordar que todas las claves se crean en HKey Classes Root, HKCR):

  1. Creamos la entrada a la extensión ".VEPX", y en el valor predeterminado, escribimos que toda la información referente a esta extensión, estará en la clave "VEncoder.VEPX".
  2. Luego creamos la clave "Vencoder.VEPX" y al valor por default le asignamos el texto que describe nuestro archivo, en este caso usamos "Archivo de perfil para VEncoder"
  3. Luego asignamos el icono que queremos usar para representar a nuestros archivos. Debemos de crear una clave llamada "DefaultIcon" y al valor por default, le asignamos la ruta en donde se encuentra nuestro icono, archivo dll o exe que lo contenga; si se usa un archivo dll o exe, hay que indicar el indice (wscript.exe 4 por ejemplo).
  4. Para agregar items en el menú contextual de Windows, dentro de la clave "VEncoder.VEPX" y en general para cualquier programa que hayamos hecho, hay quie agregar una clave llamada "Shell". Para que la opción de abrir con nuestro programa sea la primera, debemos de agregar una sub-clave llamada "Open" y al valor por default, agregar el texto que queremos mostrar en el menú contextual.
  5. Para que la entrada anterior funcione, debemos de asignar que "cosa" debe de pasar cuando hagamos clic en el. Para el caso  concreto de VEncoder, solo abriremos la aplicación, por lo que creamos una nueva sub-clave llamada "command" y en el valor debemos de asignar el programa junto con los parámetros para su ejecución, retomando la estructura anterior al código, ahí se muestran como se deben de pasar los argumentos y el nombre de archivo, sera representado por "%1". Si VEncoder aceptara parámetros (cosa que no es así por el momento), la instrucción debería de quedar similar a la siguiente: '$INSTDIR\Vencoder 2.exe "%1"', usaríamos primero las comillas simples y después las comillas dobles.
El resultado del código anterior se verá cómo este:


Para agregar entradas adicionales, solo debemos de repetir los últimos dos paso, solo cambiando el nombre de la clave:
WriteRegStr HKCR ".VEPX" "" "VEncoder.VEPX"
WriteRegStr HKCR "VEncoder.VEPX" "" "Archivo de perfil para Vencoder"
WriteRegStr HKCR "Vencoder.VEPX\DefaultIcon" "" "$INSTDIR\VEPX.ico" 
WriteRegStr HKCR "VEncoder.VEPX\shell\Open\" "" "Cargar en VEncoder 2"
WriteRegStr HKCR "VEncoder.VEPX\shell\Open\command" "" "$INSTDIR\Vencoder 2.exe"
WriteRegStr HKCR "VEncoder.VEPX\shell\Editar\" "" "Esta es una prueba"
WriteRegStr HKCR "VEncoder.VEPX\shell\Editar\command" "" "$INSTDIR\Vencoder 2.exe"

Y el resultado será como el siguiente:

Igual que en los post anteriores, el código competo lo puedes descargar de aqui.
Y bien, por ahora es todo, dudas o comentarios, puedes ponerlos en la parte de abajo.

Los leo luego.

Vamos a programar #23 - Haciendo un instalador en NSIS #2 - Datos utiles.

Hola de nuevo a todos, el día de hoy vamos a continuar con el tema de los instaladores, en el post anterior vimos lo básico de como escribir el código en NSIS. Hoy veremos unas cuantas cosas que nos ayudaran a hacer un instalador más detallado.

Tip #1 - El Desintalador.

Cuando escribimos el instalador, lo primero que nos debemos de preguntar, es si el usuario no va a cambiar de opinión, si en algun momento decide ya no usar la aplicación, querrá borrarla de su computadora. Y para esto, recurrirá al des-instalador. La manera más sencilla de hacerlo, es mediante el panel de control.
Si observamos bien, vemos que hay una entrada a nuestro des-instalador, pero no sabemos nada más.
Para poder identificar de manera más precisa cual es nuestro programa debemos de incluir información que se encargue de esa tarea.
Este es un ejeplo de un programa que muestra información que puede ser de ayuda para el usuario.
Entre los detalles más importantes que debemos de incluir, están los siguiente:
  • Nombre que identifique al des-instalador.
  • Ruta en la que se encuentra.
  • Nombre del publicador.
  • Icono.
  • Tamaño de la instalación.
  • Versión del programa.
  • Vinculo a la página del creador del programa.
  • Vinculo de ayuda del programa.
Para poder hacerlo, necesitamos agregar algunas entradas al registro. Windows tiene una seccion dedicada a los instaladores, y ademas a definido una serie de entradas por default. si incluimos la informacion de estás, windows la mostrará, si no lo hacemos, el instalador aparecerá cómo en la primer imagen; sin información.
Sabiendo lo anterior, en NSIS podemos escribir claves en el registro durante el proceso de instalación. Para agregar la información que requerimos podemos agregar código como el siguiente:

WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Vencoder" "DisplayName" "Vencoder 2.0"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "Publisher" "MDev 2015"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "DisplayIcon" "$INSTDIR\Main.ico"
${GetSize} "$INSTDIR" "/M=*.* /S=0K " $0 $1 $2
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "EstimatedSize" "$0"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "DisplayVersion" "2.1" 
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "URLInfoAbout" "http://xworkforall.blogspot.com"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "HelpLink" "http://xworkforall.blogspot.com/2016/07/el-video-correcto-2.html"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VEncoder" "NoRepair" 1


Las lineas de código scriben entradas en el registro de Windows la instruccion "WriteRegStr" escribe un valor de cadena (texto por lo general), la instruccion WriteRegDWORD, escribe un valor DWORD.
En orden de aparicion, las lineas del código anterior se usan para representar la siguiente información:

  1. Nombre que se mostrara en la entrada al des-instalador.
  2. Ruta en la que se encuentra el des-instalador, siempre debe de ir entre comillas.
  3. Información del publicador (empresa).
  4. Icono que se usara para mostrarse en la lista de programas.
  5. ${GetSize} Es una función de NSIS que se usa para obtener el tamaño de un folder o archivo. Está la veremos con mayor detalle un poco más adelante, pero en este caso se usa para obtener el tamaño de la instalación. Para que funcione es importante que previamente ya se hayan copiado todos los archivos a la carpeta de instalación.
  6. Cuando ya obtuvimos el tamaño de la instalación; está función escribe esa información. Para que funcione, el tamaño debe de estar en KiloBytes.
  7. Muestra la versión del programa.
  8. Vinculo a una dirección de internet "Acerca de".
  9. Vinculo a una dirección con ayuda para el programa.
  10. Cuando NO se tiene la opcion de modificar los componente ya instalados.
  11. Cuando NO se tiene la opcion de reparar la instalacion.
Para que des-instalador funcione, minimamente se deben de escribir las entradas "DisplayName" y "UninstallString", de ahí en fuera las otras son totalmente opcionales, pero siempre es recomendable incluir al menos todas las anteriores para tener el mínimo de información necesaria.

Al escribir todas las entradas anteriores, cuando vayamos a la seccion de des-instalar programas, nuestra aplicación tendrá el siguiente aspecto:

Y bien, es todo por hoy, en el siguiente post veremos cómo asociar una extensión con nuestro programa, recuerda que el código de este post lo puedes bajar de mi dropbox, viene con un ejemplo de un instalador para VEncoder 2.

P.D Hay que adaptar el código de VEncoder para funcionar desde "Program Files", ya que cuando se usa desde está ruta, requiere  de algunos cambios.

Los leo luego.