Xwork's Blog

The lord is waiting to take your hand.

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

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.

Vamos a programar #22 - Haciendo un instalador en NSIS.

Hola de nuevo a todos, el día de hoy les voy a mostrar el código detrás de un instalador que hice ya hace tiempo.
Cómo muchos de ustedes sabrán. Yo soy programador y he hecho programas de muchos tipos, algunos más complejos que otros. Dependiendo del grado de complejidad muchos de ellos vienen divididos, es decir; algunas funciones criticas las hago en bibliotecas DLL y al momento de distribuir el programa, cuando llega al usuario final, en ocasiones los usuarios "pierden alguna" DLL o algún recurso que el programa necesita. Inicialmente solo empaquetaba todo en un archivo ZIP y en el programa principal, programaba que hiciera los cambios en el sistema que fueran necesarios para que tdo funcionara correctamente, además siempre incluía un manual de ayuda (que al parecer nadie leía) para que el usuario pudiera arreglar los problemas comunes.
Pero cómo siempre aparecían errores decidí buscar una manera de minimizarlos lo más posible. Para eso busque la forma de hacer un instalador, y poder distribuir todo lo necesario en un solo paquete y además hacer los ajustes necesarios para que el usuario solo ejecutará la aplicación y evitar disgustos.

Al inicio probé con una herramienta que se llama Create Install Free, al inicio era gratuita, funcionaba de maravilla, permitía agregar y registrar bibliotecas en windows, fuentes y controles OCX, era una solución muy buena. Tiempo después paso a tener costo, y me hubiera gustado comprarlo, pero no tenía una tarjeta de crédito valida para realizar la compra. Entonces dejé de usarlo y busqué otra forma de crear un paquete de instalación.

Así fue cómo llegué a NSIS.


Nullsoft Scriptable Install System (NSIS) es un "manejador de script" Windows de código abierto con requerimientos mínimos, desarrollado por Nullsoft, los creadores de Winamp. NSIS ha crecido en popularidad como una alternativa al uso extenso de productos comerciales como InstallShield y es actualmente utilizado para un sin número de aplicaciones distribuidas a través de Internet.
NSIS es liberado bajo una combinación de licencias de software libre, principalmente la licencia de zlib/libpng, de esta forma haciendo a NSIS software libre..

Creando un instalador.

Lo primero que vamos a hacer es descargar el programa, una vez que está descargado e instalado, vamos a escribir un archivo de texto con notepad++ o con cualquier editor de texto plano.
Hoy veremos directamente cómo hacer uso de Nsis Modern User Interface. Prefiero ir directamente a está parte porque la mayoría de las funciones o procesos que vamos a realizar,  se pueden llevar a cabo con el uso de este plug-in que viene integrado por default. En adición, este se encarga de que todas las ventanas del instalador tengan la misma apariencia que el sistema operativo.

Una vez que tenemos todo listo vamos a empezar a escribir el código. Tomemos está parte de código y revisemos cada una de las partes que lo componen.

!include "MUI2.nsh"
!include "FileFunc.nsh"
!include "Library.nsh"

SetCompressor lzma
SetOverwrite on
SetDatablockOptimize on
!define VERSION "2.0"
Name "VEncoder 2"

OutFile "Setup.exe"
InstallDir "$PROGRAMFILES\Vencoder 2"
InstallDirRegKey HKCU "Software\MPM\VENCODER 2" ""
ShowInstDetails show
RequestExecutionLevel admin
CRCCheck on
XPStyle on

La descripcion del código anterior es la siguiente:

  • Lo primero que hacemos es incluir "MUI2.nsh" que es la "librería" que contiene todas las funciones del plugin MUI, en NSIS cuando se quiera incluir una librería, se debe de hacer usando la palabra reservada "!include" seguida por el nombre del plug-in ó "librería", para NSIS, estás tienen una extension "nsh".
  • SetCompressor, selecciona el compresor que se usará, en este caso usaremos el compresor LZMA, pero ademas de este, podemos usar ZLIB o BZIP2, por lo general yo uso este porque a mi criterio; ofrece mayores niveles de compresión..
  • SetOverWrite, Sirve para indicar si se deben de sobre-escribir los archivos que ya existan en el lugar de destino..
  • SetDataBlockOptimize. Sirve para hacer optimizaciones en el bloque de datos. Si este ya contiene un dato, en lugar de agregarlo de nuevo, simplemente se hace referencia a el. Puede ayudar a ahorrar un poco de espacio. (NSIS: SetDataBlockOptimize)
  • Definimos la versión de la aplicación.
  • Establecemos el nombre (es este caso VEncoder 2)
  • Establecemos el nombre de salida para el instalador una vez que ya está compilado. Puede ser cualquiera.
  • Establecemos cual va a ser el directorio de salida para los archivos que se van a instalar, hacemos uso de $PROGRAMFILES, que es una palabra reservada de nsis que generalmente se refiere a "C:\Program Files (x86)\" o la carpeta que contiene todos los programas instalados.
  • InstallDirRegKey. Obtiene la ruta de instalación del registro de windows. Cabe aclarar que esta solo tendrá un valor SOLO si anteriormente ya hemos instalado algún programa que haga uso de esta.
  • ShowInstDetails. Cuando se lleva a cabo el proceso de instalación, muestra cual es archivo que se está copiando.
  • RequestExecutionLevel. Indica el nivel de ejecución que requiere el instalador. Pueden ser los siguiente valores: none, user, highest, admin.
  • CRCCheck. Comprueba la integridad del instalador antes de empezar la instalación.
  • XPStyleOn. Habilita que el instalador tenga la apariencia de Windows.

Por lo general el pedazo de código anterior es un buen ejemplo de cómo empezar un script, solamente debes de modificar las partes correspondientes a tu aplicación a instalar.

Ahora sigue definir todas las páginas que componen el instalador.
Página de selección de componentes.
Cada instalador está compuesto por páginas, cada una de ellas tiene la función de guiar paso a paso al usuario. Para hacer uso de ellas hay que "declararlas" en código de nsis. Un instalador "básico" puede usar el siguiente código para definir las páginas más comunes.

!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "COPYING.txt"
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "Spanish"

El código anterior crea las siguientes páginas:
MUI_PAGE_WELCOME: Crea la página que contiene un mensaje de bienvenida.
MUI_PAGE_LICENSE "COPYING.txt": Crea la página que muestra un archivo de texto llamado "Copying.txt" cómo licencia, se puede usar cualquier fichero de texto y asignarlo cómo parámetro.

MUI_PAGE_COMPONENTS: Muestra una página que permite seleccionar que secciones son las que queremos instalar.


MUI_PAGE_DIRECTORY: Muestra la página que nos permite la selección de un folder para llevar a cabo la instalación.

MUI_PAGE_INSTFILES: Muestra el progreso de la instalacion.


MUI_PAGE_FINISH: Muestra un mensaje al finalizar la instalación.

Ademas las siguientes intrucciones MUI_UNPAGE_WELCOME, MUI_UNPAGE_INSTFILES crean las páginas correspondientes en el desisnstalador.
Luego hay que indicar que todas las páginas usan el idioma español, para eso usamos la siguiente instruccion MUI_LANGUAGE "Spanish", en donde "spanish" puede ser cualquier idioma soportado por NSIS,
Si miramos la ultima imagen, veremos un texto que esta en negritas y dice: "Las instalación ha terminado". Este string hay que definirlo, de hecho antes de crear las páginas, hay que definir los strings para todas las partes de cada pagina, el código que hace eso no lo pongo aquí, porque haria muy extenso el Post, pero al final agrego el código fuente para que lo revises.

NSIS trabaja con apartados llamados secciones, en ellas es donde se agregan los archivos que queremos incluir en la instalación, la ventaja de esto es que podemos crear varios apartados en los cuales se incluyan los archivos que son realmente importantes en la aplicación y otra sección en la que se incluyan archivos miscelanéos.
Para este caso tenemos tres secciones, 2 de ellas obligatorias y una de ellas opcional.

Section "Principal" MainSec
##Indicamos que está seccion es obligatoria
SectionIn RO
##Establecemos la carpeta de destino
SetOutPath "$INSTDIR"
File "Copying.txt"
File "Main.Ico"
File "MediaInfo.dll"
File "MediaInfoNET.dll"
File "Vencoder 2.exe"
File "Vepx.ico"
File "ListViewEx.dll"
SetOutPath $INSTDIR\Doc
File /r doc\*.*
SectionEnd

Section "Archivos adicionales" AdditionalSec
SetOutPath "$DOCUMENTS\Profiles"
File "MB300.Vepx"
File "PSP.vepx"
File "PSVita.vepx"
File "Screen HD.Vepx"
File "Screen FHD.Vepx"
SectionEnd

Con el código previo, definimos dos secciones, cada "section" recibe 2 parámetros, uno de ellos es una descripción y el otro es un nombre que servirá para identificar cada una de las secciones.
Para la primera sección indicamos que es obligatoria con "SectionIn RO" cuando se establece, en la página de selección de componentes, aparecerá un casilla de selección de solo de lectura, cuando no se incluye, el usuario puede marcar o des-marcar la casilla de selección, así indicando que esa sección no se quiere instalar.
En la seccion "MainSec", después de que establecimos que sea obligatoria, agregamos todos los archivos necesarios de nuestra aplicación, primero establecemos que todos se copiaran a la carpeta de instalacion "$INSTDIR" ("C:\Program Files (x86)\VENcoder2\"), luego ponemos una lista en la que cada archivo inicia con la palabra clave "File" seguida por el nombre del archivo que queremos incluir, un poco más abajo encontramos la instrucción "SetOutPath" nuevamente con la ruta "$INSTDIR\Doc", esto indica que se debe de cambiar la salida de los archivo a está nueva dirección, luego al comando "File" le agregamos el parámetro "\r" que indica que buscara de forma recursiva lo archivos en la carpeta doc.

Cada seccion debe de incluir una descripcion, para definirlas, se usa código similar al siguiente:

!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${MainSec} "Contiene todos los archivos escenciales de la aplicacion"
!insertmacro MUI_DESCRIPTION_TEXT ${LinkSec} "Accesos directos a la aplicacion)"
!insertmacro MUI_DESCRIPTION_TEXT ${AdditionalSec} "Archivos de perfil con las configuraciones mas frecuentes (opcional)"
!insertmacro MUI_FUNCTION_DESCRIPTION_END

Despues llamamos a las macros que se encarhgan de crear las descripciones; MUI_FUNCTION_DESCRIPTION_BEGIN, recibe dos parámetros, uno de ellos es el nombre de la seccion a la cual se quiere poner su descripción y el segundo, es la cadena de texto.

Lo que se mostró en el post son las partes principales, pero el código completo junto con lo archivos, como siempre; lo dejo en mi dropbox para que lo descargues y lo revises. Incluye todo lo necesario para crear un instalador funcional para VEncoder.
En el siguiente Post vamos a ver alguna funciones no vitales, pero que resultan muy útiles.

Por ahora es todo, los leo luego

Vamos a programar #17 - Extraer una imagen embebida en un MP3

Hola a todos de nuevo. El día de hoy vamos a terminar con el temas de las imágenes embebidas en los archivos MP3.
En el post anterior, hicimos una parte del código de C# encargado de hacer la extracción. Había quedado pendiente el trabajo para la versión 2 de las etiquetas ID3.

Hay que recordar que la esctructura del la etiqueta "PIC" es la siguiente:

Attached picture "PIC"
Frame size $xx xx xx
Text encoding $xx
Image format $xx xx xx
Picture type $xx
Description <textstring> $00 (00)
Picture data <binary data>
Basados en la información anterior crearemos el código necesario.

El código para la versión 2.

Retomando el codigo anterior, solo hay que agregar lo que nos hace falta. La función ImageContent constaba de 3 condiciones if importantes (ademas de algunos secundarios), el primero y principal consiste en verficar si hay una etiqueta ID3 valida, si la había, el siguiente if comprobaba que la version fuera la 2.3, el siguiente comprobaba para la version 2. Está fue la que quedo pendiente, el siguiente codigo va inmediatamente despues del if que comprobaba para la version 2.3.


 if (MyTagType == TagVersion.TagVer22)
                    {
                        {
                            //PIC
                            BinRead.BaseStream.Position = PicTagPos + 3;
                            //Tamaño
                            TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256;
                            TotalLenght = TotalLenght + BinRead.ReadByte() * 256;
                            TotalLenght = TotalLenght + BinRead.ReadByte();
                            txtInfo.Text = txtInfo.Text + "\n La etiqueta PIC esta en la dirección: " + PicTagPos.ToString() + "\n";
                            txtInfo.Text = txtInfo.Text + "con longitud de" + TotalLenght.ToString() + " bytes\n";
                            //TextEncoding
                            BinRead.ReadByte();
                            //Type JPG o PNG
                            string MType = "";
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            txtInfo.Text = txtInfo.Text + "\nDel tipo " + MType;
                            //Tipo de imagen
                            BinRead.ReadByte();
                            //Reciclamos el flag anterior
                            IsNotZero = true;
                            string ImgDescription = "";
                            //Descripcion de la imagen
                            while (IsNotZero == false)
                            {
                                byte Val = BinRead.ReadByte();
                                if (Val != 0x00)
                                {
                                    ImgDescription = ImgDescription + string.Concat((char)Val);
                                    IsNotZero = false;
                                }
                                else
                                {
                                    IsNotZero = true;
                                }
                            }
                            long ImageFileBegin = BinRead.BaseStream.Position;
                            int ImageLenght = (TotalLenght - 1 - ((int)ImageFileBegin - PicTagPos - 6));
                            txtInfo.Text = txtInfo.Text + " La imagen empieza en " + ImageFileBegin.ToString();
txtInfo.Text = txtInfo.Text + "Y mide " + ImageLenght.ToString();
                            //Creamos una imagen desde el stream y la asignamos al picbox
                            //Ponemos el puntero al inicio de la imagen
                            BinRead.BaseStream.Position = ImageFileBegin + 1;
                            using (MemoryStream MS = new MemoryStream(BinRead.ReadBytes(ImageLenght)))
                            {
                                PicArt.Image = Image.FromStream(MS);
                                using (FileStream file = new FileStream(SaveLocation + "." + ImageType(MType), FileMode.Create, FileAccess.Write))
                                {
                                    MS.WriteTo(file);
                                    file.Flush();
                                    file.Close();
                                }
                                MS.Close();
                            }
                        }
                    }
}

Si revisamos el código de forma detenida veremos que es mu similar al código anterior. Entonces te preguntarás ¿No es un desperdicio? Si y no, el código está pensado para hacer una demostración de lo que hicimos en el primer post? Y la respuesta es: NO, lo que trate de hacer con estos post es demostrar como es que se puede portar una situación al código así "tal cual", porque de hecho lo que hicimos aquí, fue lo mismo que hicimos con el programa XVI32. Es decir lo que primero hicimos a mano, luego lo plasmamos en el código.

Información para una canción con etiqueta PIC (Ver 2)

Información para una canción con etiqueta APIC (Ver 2.3 {Lo admito soy un Pokefan!!!})
 En el próximo post crearemos una función para procesar por lotes y liberaré la aplicación completa, por ahora es todo.

Los leo luego.

Vamos a programar #16 - Extraer una imagen embebida en un MP3

Hola de nuevo a todos, han pasado algunos días desde la ultima vez que hice un post y finalmente le traigo uno nuevo. Cómo recordaran, en el post anterior vimos como extraer una imagen embebida en un mp3, pero en esa ocasión lo hicimos manualmente. En este post vamos a hacer un programa en c# que se encargue de hacer de forma automática ese proceso.

El código en c#.

Primero que nada haremos un pequeño repaso a que es lo que vamos a hacer.
  1. Abrimos un archivo mp3.
  2. Leemos una fracción del archivo para determinar si existe o no la etiqueta "APIC" o "PIC".
  3. Si existe Busacmos su posicion.
  4. Leemos el tamaño total de la etiqueta.
  5. Leemos los datos adicionales (tipo, codificacion, etc).
  6. Leemos el archivo de la imagen.
  7. Guardamos el archivo de imagen.
  8. Listo.
Primero crearemos una nueva aplicación de windows forms, yo en este caso usare #Develop
Ahora agregaremos solo 5 controles:
  • TextBox
    • (name) = TxtPath
  • Button
    • (name) = BtnOpen
    • Text = "Abrir"
  • Button
    • (name) = BtnExtract
    • Text = Extraer
  • TextBox
    • (name) = TxtInfo
    • ScrollBars = Vertical
    • Multiline = True
  • PictureBox
    • (name) = PicArt

Ya con los controles vamos a agregar código para hacer funcionar las cosas. En primer lugar, hay que recordar que hay más de una version de las etiquetas ID3, hasta el momento en que se creo este post existian las versiones: 1, 2, 2.3 y 2.4, nosotros solo nos ocuparemos para los casos de las versiones 2 y 2.3 (y 2.4 ya que para el caso de las etiquetas no hay cambios tan grandes con respecto a la versión anterior).
Basados en lo anterior, lo primero que debemos de hacer es identificar en que versión estamos trabajando, para eso crearemos una función que se encargue de devolver la versión. Para evitar que se usen valores desconocidos, en primer lugar crearemos una enumeración que contenga solo los valores posibles para nuestro proyecto.

private enum TagVersion
{
 TagVer22,
 TagVer23,
 TagVerUnknow
}

En la enumeración anterior se definen situaciones para la versión 2 y 2.3, cualquier otra versión devolverá el valor  "TagVerUnknow". Teniendo una definición para los valores que podemos esperar, ahora si creamos una función que devuelva uno de los valores de la enumeración.


private TagVersion GetId3Ver(byte[] Data)
{//49 44 33
	if (Data[0] == 0x49 && Data[1] == 0x44 && Data[2] == 0x33)
	{
		if (Data[3] == 2) return TagVersion.TagVer22;
		if (Data[3] == 3) return TagVersion.TagVer23;
		else return TagVersion.TagVerUnknow;
	}
	else
	{
		return TagVersion.TagVerUnknow;
	}
}

La funcion GetId3Ver recibe cómo parámetro una matriz de bytes, después lee los primeros 3 valores y compra si corresponden a "ID3", si si lo son, después leen el cuarto byte y regresan el la versión de acuerdo al valor de este. hay que recordar que cuando hay una etiqueta ID3, los primero bytes describen la etiqueta en si y el cuarto byte nos dice que versión es; 2 para la versión 2, 3 para la versión 2.3 y 4 para la versión 2.4.

Ahora vamos a crear la función que se encargará de leer toda la información y guardarla en un archivo a parte.

private void ImageContent(string FilePath,string SaveLocation)
{
	Byte[] BinSearchData;
	Stream FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read,FileShare.Read);
	using(BinaryReader BinRead = new BinaryReader(FS,Encoding.Default))
	{
		BinSearchData = BinRead.ReadBytes(1024);
		TagVersion MyTagType;
		string MyTag;
		switch (GetId3Ver(BinSearchData))
		{
			case TagVersion.TagVer22:
				MyTag = "PIC";
				MyTagType = TagVersion.TagVer22;
				break;
			case TagVersion.TagVer23:
				MyTag = "APIC";
				MyTagType = TagVersion.TagVer23;
				break;
			default:
				MyTag = "Error";
				MyTagType = TagVersion.TagVerUnknow;
				break;
		}
		Match FindTag = Regex.Match(Encoding.ASCII.GetString(BinSearchData), MyTag);
		if (FindTag.Success)
		{
			int TotalLenght = 0;
			int PicTagPos = FindTag.Index;
			if (MyTagType == TagVersion.TagVer23)
			{
				bool IsNotZero = false;
				BinRead.BaseStream.Position = PicTagPos + 4;
				//Size
				TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256 * 256;
				TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256;
				TotalLenght = TotalLenght + BinRead.ReadByte() * 256;
				TotalLenght = TotalLenght + BinRead.ReadByte();
				txtInfo.Text = txtInfo.Text + "\n La etiqueta APIC esta en la dirección: " + PicTagPos.ToString() + "\n";
				txtInfo.Text = txtInfo.Text + "con longitud de" + TotalLenght.ToString() + " bytes\n";
				//Flags, por ahora no nos interesan
				BinRead.ReadByte();
				BinRead.ReadByte();
				//TextEncoding
				BinRead.ReadByte();
				//Type
				string MType= "";
				while (IsNotZero == false)
				{
					byte Val = BinRead.ReadByte();
					if (Val != 0x00)
					{
						IsNotZero = false;
						MType = MType + string.Concat((char)Val);
					}
					else
					{
						IsNotZero = true;
					}
				}
				txtInfo.Text = txtInfo.Text + "\nDel tipo " + MType;
				//Tipo de imagen
				BinRead.ReadByte();
				//Reciclamos el flag anterior
				IsNotZero = true;
				string ImgDescription = "";
				//Descripcion de la imagen
				while (IsNotZero == false)
				{
					byte Val = BinRead.ReadByte();
					if (Val != 0x00)
					{
						ImgDescription = ImgDescription + string.Concat((char)Val);
						IsNotZero = false;
					}
					else
					{
						IsNotZero = true;
					}
				}
				long ImageFileBegin = BinRead.BaseStream.Position;
				int ImageLenght = (TotalLenght- 1 - ((int)ImageFileBegin - PicTagPos - 10));
				txtInfo.Text = txtInfo.Text + " La imagen empieza en " + ImageFileBegin.ToString();
				txtInfo.Text = txtInfo.Text + "Y mide " + ImageLenght.ToString();
				//Creamos una imagen desde el stream y la asignamos al picbox
				//Ponemos el puntero al inicio de la imagen
				BinRead.BaseStream.Position = ImageFileBegin + 1;
				using (MemoryStream MS = new MemoryStream(BinRead.ReadBytes(ImageLenght)))
				{
					PicArt.Image = Image.FromStream(MS);
					using (FileStream file = new FileStream(SaveLocation + "." + ImageType(MType), FileMode.Create, FileAccess.Write))
					{
						MS.WriteTo(file);
						file.Flush();
						file.Close();
					}
					MS.Close();
				}
			}
			if (MyTagType == TagVersion.TagVer22)
			{
				//Lo mismo que para la anterior, solo ajustando los datos, queda de tarea
			}
		}
		else
		{
			ErrPr.SetError(BtnOpen, "No hay etiqueta APIC");
		}
	}
	FS.Close();
}

Hacemos exactamente lo que hicimos cuando extrajimos la imagen a mano, primero leeremos el archivo, buscamos la etiqueta y luego leemos el archivo, determinamos el tipo de imagen que es, para eso usaremos la siguiente funcion:
private string ImageType(string MIMEType)
{
	//PNG or JPG
	//image/png or image/jpeg
	if (MIMEType == "PNG" || MIMEType == "image/png")
		return "png";
	if (MIMEType == "JPG" || MIMEType == "image/jpeg")
		return "jpg";
	else
		return "bin";
}

Está función solo devuelve "PNG" o "JPG" y se usa al momento de guardar el resultado; es decir al momento de construir el nombre de archivo.

Finalmente solo queda mandar a llamar a la función "ImageContent" desde el botón BtnExtract, la función guardara la imagen con el mismo nombre de archivo que el del MP3 que usemos y también mostrará la imagen en el picture box "PicArt"

Ahora solo queda hacer todo el procedimiento para la versión 2, pero eso lo haremos en el siguiente Post, ademas crearemos una función para agregar muchos archivos y realizar la extracción por lotes. Te recomiendo que le eches un vistazo a la documentación parta que sea más fácil entender cómo es que funciona.

Por el momento es todo, los leo luego.

Vamos a programar #13 - El código de VEncoder 2

El día de hoy vamos a continuar con la revisión del código de VEncoder,

Funcion DoWork.

El Código de la función DoWork es el siguiente:

/// <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.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.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();

}



La función DoWork inicia la ejecución de FFMpeg indicándole todos los parámetros para realizar la conversión de todos los archivos que se contenían en la lista, recibe dos parámetros: el primero, una lista del tipo string; que contiene todas las lineas de comandos generadas previamente con la función BuildCommandLine junto con la funcion PrepareWork.
El segundo parámetro es una lista del tipo int, que contiene la duración de los archivos en segundos.
Esta función se ejecuta dentro de la funcion PrepareWork  si se pasa el parámetro booleano HardWork cómo true.
Para la ejecución de FFMpeg se usa un Thread y se implementa de forma básica, dentro de la ejecución de este hilo, creamos un proceso que sera el encargado de abrir FFMpeg, ademas de que le diremos que todo la salida la redireccione a nuestro programa para poder extraer los datos que necesitamos.
Debido a que la conversión se hace en lotes, se usa un ciclo for para recorrer todos los elementos contenidos en la lista "Params" y ya dentro de ese mismo ciclo, se usa un bucle while que estará activo mientras nuestro proceso (ffmpeg) no haya finalizado o mientras el proceso no devuelva una cadena vacía.
Para cancelar enteramente el proceso usamos dos comprobaciones, una de ellas en el ciclo for y otra en el bucle while, esto para poder detener en cualquier parte. Si la interrupción se da cuando se esta ejecutando el bucle while, al salir de este continuara en el ciclo for y nuevamente iniciaría el bucle while si aun quedan elementos en la lista Params, entonces para evitar esto, lo único que hacemos es crear las interrupciones en ambos lugares.
Todas las instrucciones "invoke" se usan para interactuar con la interfaz principal, siempre le resulta al usuario útil saber cuanto es el progreso que se lleva.
En esta función se usa de forma muy básica todas las características de la clase Thread, en realidad es muy potente, pero para fines prácticos solo usamos lo mas importante: iniciar y detener Threads, analizando todos los elementos contenidos en esta clase, podemos encontrar que se puede tener un control más preciso de cada Thread: su estado, si responde o no, etc.
Lo mismo sucede con la clase Procces, pero por ahora solo usamos los métodos que están implementados en la función.

Funcion SetInfo.

El codigo de la funcion SetInfo es el siguiente:


         /// <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();
         }

La función SetInfo se encarga de obtener la información de los archivos. La información incluye: Tamaño, resolución, cuadros por segundo, aspecto, velocidad de bits, etc.
Recibe como parámetro una lista del tipo string que contiene las rutas de los archivos de los cuales se quiere obtener la información.
Al igual que en la función anterior, se usa un Thread para hacer este proceso, esto debido a que cuando se procesan algunos archivos, dependiendo de sus características, puede tomar un poco de tiempo para obtener la información, y esto causaría que la interfaz principal se bloquee, para evitar esto, se inicia un thread diferente y se usa el TaskBar de windows para mostrar el progreso. Hay resaltar que debido a que se usan características disponibles a partir de Windows vista, no es posible hacer compatible la aplicación con versiones anteriores a vista.
Finalmente esta función agrega toda la información a lvfilesin, que es un ListView de la interfaz principal.

Y bien, esas son todas las funciones que componen a VEncoder, como verás, no es tan complejo y el resultado es muy útil, en el siguiente Post vamos a implementar una nueva función que apagará el equipo cuando todas la conversiones se hayan realizado.

Por ahora es todo, nuevamente les agradezco por el interés en el código de VEncoder y por el apoyo recibido en el post anterior.

Los leo luego.

Vamos a programar #12 - El código de VEncoder 2

El día de hoya vamos a continuar con la revisión del código de vencoder, en los post anterior ya vimos como es que el programa interactua con FFMpeg, el día de hoy veremos un par de funciones que también lo hacen.


Funcion PrepareWork



El código de la función PrepareWork es el siguiente:

/// <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, "23.974", CBOBitsA.Text, CboFreq.Text, CBOAChannels.Text, TXTSavePath.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, "23.974", CBOBitsA.Text, CboFreq.Text, CBOAChannels.Text, TXTSavePath.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, CboFreq.Text, CBOBitsA.Text, "44100", CBOAChannels.Text, TXTSavePath.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, "23.974", CBOBitsA.Text,CboFreq.Text, CBOAChannels.Text, TXTSavePath.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, "23.974", CBOBitsA.Text, CboFreq.Text, CBOAChannels.Text, TXTSavePath.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, CboFreq.Text, CBOBitsA.Text, "44100", CBOAChannels.Text, TXTSavePath.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";

}

}

}

La función PrepareWork lo que hace es construir la linea de comandos para cada archivo que se encuentre en la lista, dependiendo si se le indicó al programa o no si se quiere hacer la conversion en dos pasadas, crea la linea de comandos lista para usarse.
La función PrepareWork recibe un parámetro del tipo booleano que indica que es lo que se debe de hacer después de crear todas las lineas de comandos. Si el valor del parametro HardWork se establece en true; al terminar de procesar todos los ajustes, llama a la funcón DoWork que es la encargada de iniciar la ejecución FFmpeg.
La construcción de la linea de comandos se realiza a la "mala"; es decir va recorriendo uno a uno los controles para obtener sus propiedades (Text por ejemplo). Ademas de que por ejemplo la cantidad de cuadros por segundo está fija, para evitar eso, en lugar de usar 23.974, podemos usar un TextBox en lugar de 23.974.
Esta función solo "recolecta" todos los datos y lo pone juntos, pero la función que realmente hace el trabajo sucio es la funcion BuildCommandLine.

Funcion BuildCommandLine.

El código de la funcion BuildCommandLine es el siguiente:


 /// <summary>
 /// Genera la linea de comados para usarse con FFMPEG
 /// </summary>
 /// <param name="InFile">Archivo de entrada</param>
 /// <param name="VCodec">Codec de video a usarse</param>
 /// <param name="VideoSize">Resolucion del video</param>
 /// <param name="VideoBit">Velocidad de bits del video</param>
 /// <param name="VAspect">Ratio para el video</param>
 /// <param name="VProfile">Perfil para usarse en el video</param>
 /// <param name="FrameRate">Cantidad de cuadros por segundo para el video</param>
 /// <param name="AudioBit">Veocidad de Bits para el audio</param>
 /// <param name="AudioFreq">Frecuencia de muestreo para el audio</param>
 /// <param name="AChannels">Numero de canales para el audio</param>
 /// <param name="OutPath">Donde se gurdara la salida; debe de ser una ruta a un carpeta</param>
 /// <param name="ForFirstPass">La linea de comandos se hará para la primera pasada de dos</param>
 /// <param name="AfterPass1">La Linea de comandos se hará para la segunda pasada</param>
 /// <param name="SinglePass">La linea de comandos se hara para una unica pasada</param>
 
 private string BuildCommandLine(string InFile,string VCodec,string VideoSize,string VideoBit,string VAspect,string VProfile,string FrameRate,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,"\""," -c:v ",VCodec," -s ",VideoSize, " -b:v ",VideoBit,"k -aspect ",VAspect," -profile:v ",VProfile," -threads 0 -r ",FrameRate,
                     " -level 4.1 -crf 22 -f mp4 -pass 1 -an NUL");
 }
 if(AfterPass1){
 return string.Concat("-y -i ","\"",InFile,"\""," -c:v ",VCodec," -s ",VideoSize, " -b:v ",VideoBit,"k -aspect ",VAspect," -profile:v ",VProfile," -threads 0 -r ",FrameRate,
                     " -level 4.1 -crf 22 -f mp4 -pass 2 -c:a libvo_aacenc -b:a ",AudioBit,"k -ar ", AudioFreq," -ac ",AChannels," \"",OutPath,"\\",GetFileName(InFile),".mp4","\"");
 }
 if (SinglePass) {
 return string.Concat("-y -i ","\"",InFile,"\""," -c:v ",VCodec," -s ",VideoSize, " -b:v ",VideoBit,"k -aspect ",VAspect," -profile:v ",VProfile," -threads 0 -r ",FrameRate,
                     " -level 4.1 -crf 22 -f mp4 -c:a libvo_aacenc -b:a ",AudioBit,"k -ar ", AudioFreq," -ac ",AChannels," \"",OutPath,"\\",GetFileName(InFile),".mp4","\"");
 }else{
 return "Test";
 }
 }

La funcion BuildCommandLine se encarga de crear la linea de comandos para usarse con FFMpeg, recibe 14 parametros y regresa un valor del tipo string con los datos listos para usarse.

  1. InFile. Es el archivo para el cual queremos crear la linea de comandos.
  2. VCodec. Es el codec que vamos a usar para el archivo, aunque ahora solo esta configurado para que la linea de comandos sea compatible  con el codec X264, mas adelante la modificaremos para agregar soporte a otros codecs (Xvid,MPEG4, etc)
  3. VideosSize. Es la resolución que tendrá el vídeo, puede ser cualquier valor que tenga la forma WWWWxHHHH, donde W es el largo y H es el ancho (1280x720 es un valor valido).
  4. VideoBit. Es la cantidad de bits que se usaran por segundo.
  5. VAspect. Indica el aspecto que deberá tener el vídeo (16:9).
  6. VProfile. Es el tipo de perfil que debe de tener el vídeo.
  7. FrameRate. Sirve para indicar la cantidad de cuadros por segundo que se deben de usar.
  8. AudioBit.Cantidad en bits para el audio
  9. AudioFreq. La frecuencia del audio en Hz.
  10. AChannels. Cantidad de canales de audio que debe de tener el vídeo.
  11. OutPath. La ruta en donde se guardaran los vídeos.
  12. ForFirstPass. Este parámetro indica como se crea la linea de comandos, si el parámetro es true, se crea la linea de comandos para la primer pasada
  13. AfterPass1. Este parámetro indica como se crea la linea de comandos, si el parámetro es true, se crea la linea de comandos para la segunda pasada
  14. SinglePass. Crea la linea de comandos para proceso de una sola pasada.
Al igual que la función anterior está diseñada para hacer la linea de comando por un archivo a la vez, la función anterior, usaba está como base y creaba la linea de comandos para todos los archivos. Al usar la función, se debe de tener cuidado con los valores que se usan al final (parametros), por ejemplo si se usa lña linea de comandos para hacer la conversion de un video en una sola pasada, los 3 parametros finales, no pueden establecerse en true, ya que modificaría valores que ya se tenían previamente.
La forma mas adecuada para manejar para que tipo de "pasada" la linea de comandos es crear una enumeracion y definir solo los casos posibles por ejemplo:

private enum CommandType

{
ForPass1,
ForPass2,
ForSinglePass
}


Y solo restaría adaptar la condiciones para cada caso.
Bien, por ahora es todo, recomiendo revisar el código para que entiendas de lo que hablo.
Los leo luego.

Vamos a programar #11 - El código de VEncoder 2

El día de hoy vamos a continuar revisando el código de VEncoder2, la vez anterior ya habíamos revisado el código de las primeras 4 funciones. Normalmente vemos las funciones de 2 en 2 pero hoy, vamos a ver un poco mas debido a que algunas de las funciones son realmente simples.


Funcion Porcentaje

El código de la funcion porcentaje es el siguiente:

/// <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;
}

Lo único que hace esta función es regresar el valor equivalente al porcentaje actual, recibe dos parámetros del tipo Single ValorActual y ValorTotal, y después solo hace la división entre ValorActual y ValorTotal, luego lo multiplica por 100, el valor resultante es el porcentaje de ValorActual con respecto a ValorTotal.


Funcion GetFileName

El código de la función GetFileName es el siguiente:
/// <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];
}


La funcion GetFileName sirve para obtener el nombre de archivo de una ruta completa.es decir, si tenemos la ruta a  un archivo cómo la siguiente: "c:\users\Ra\Desktop\Mi video.mp4" al usar la función obtenemos "Mi video.mp4". Esta función recibe un parámetro del tipo string, después la separa usando la función "split", la función split recibe como parámetro del tipo char en este caso es "(char)92", lo que hace es convertir el numero 92 a el tipo char y es equivalente a "\" (backslash). Se usa la variable TMPData  para almacenar el resultado de la función split que aplicamos sobre el parámetro de entrada. Finalmente solo regresa el ultimo valor de la matriz.
A lo mejor puedes pensar que haciendo eso nos complicamos demasiado porque dentro de las fucniones que vienen incluidas en .NET, dentro del espacio de nombre System.IO hay una funcion que hace exactamente lo mismo. Path.GetFileName hace lo mismo, pero debido a que es codigo que vengo arrastrando desde VB6 (solamente adaptado a C#), preferi incluirlo, pero sientete libre de usar la función que mas quieras.

Funcion GetPositionFronFFLine.

El código de la funcion GetPositionFromFFLine es el siguiente:

         /// <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)
         {
             try
             {
                 if (FFText.StartsWith("FRAME", StringComparison.OrdinalIgnoreCase)) {
                     string PreResult;
                     PreResult = FFText.Split((char)61)[5].ToString().Trim();
                     return (Single)TimeSpan.Parse(PreResult.Remove(PreResult.Length - 7, 7).Trim()).TotalSeconds;
                 }else{
                     return 0;
                 }
             } catch (Exception) {
                 return 1;
             }
         }

La función GetPositionFromFFLine extrae el tiempo actual de la conversion, para entender como funciona, primero echemos un vistazo a una linea de resultado de FFmpeg.

 frame= 2528 fps=167 q=27.0 size=    5246kB time=00:01:43.27 bitrate= 416.1kbits/s 
La anterior es una linea de ffmpeg cuando esta haciendo una conversión, ahora si la observamos bien, vemos que esta compuesta por 6 partes:

  1. frame= 2528, indica el frame actual de la conversión.
  2. fps=167, cantidad de cuadros por segundo a la que se hace la conversión.
  3. q=27.0, calidad
  4. size = 5246, tamaño en Kbytes.
  5. time = 00:01:43.27, tiempo actual de la conversión
  6. bitrate= 416.1kbits/s , bitrate actual.
La parte que nos interesa es la parte que marca la posicion del tiempo en la que se encuentra la conversión. para eso lo que hacemos es pasar la linea de ffmpeg (mas adelante vermos como redirigir la salida de ffmpeg a nuestro programa) cómo parámetro a la función GetPositionFromFFLine.
Primero comprueba que la línea empiece con "FRAME", luego, separa la parte del tiempo, en este caso al usar la funcion split con parametro "(char)61" (simbolo de igual "="), luego convierte el tiempo a segundos y lo regresa como resultado.
Obviamente esta funcion no es a prueba de errores, de hecho aumimos que el tiempo estará formateado de la forma HH.MM.SS.CC, pero sei en algun momento alguien intentara convertir un vide de 100 horas de duracion, VEncoder simplemente marcaría un error.
Igual que la funcion anterior, no esta optimizada, lo ideal seria usar expresiones regulares, pero por ahora esto, más adelante actualizaremos todas las funciones.

Por ahora es todo. Los leo luego.

Vamos a programar #9 - El Código de VEncoder 2.

El día de hoy; y como lo prometí hace algunos días, les traigo el código fuente de VEncoder 2.
Las entregas se van a repartir en las siguientes:

  1. El código de VEncoder2.
  2. Porqué funciona cómo funciona.
  3. Agregando funciones que necesito.
  4. Errores, Intentando arreglarlos.

El Código de VEncoder2.

Lo primero que vamos a hacer es echarle un vistazo al código de fuente, tratare de explicar un poco que es lo que hace cada parte del código. Algo importante que debo mencionar es que el código ya tiene "sus ayeres", originalmente estaba planeado para uso personal, así que el código puede estar "sucio". Si eres algo curioso, de seguro te preguntaras ¿VEncoder2, donde esta el 1?, la respuesta es simple, nunca vio la luz del sol, y no porque fuera malo u otra cosa, simplemente no se me ocurrió que le resultara interesante a alguien más a parte de mí, de hecho la idea de hacer un blog surgió a partir de alguien a quien siempre le agradeceré... Pero eso es otra historia, sigamos en los que estábamos.

Inicialmente estaba pensado para convertir los vídeos y que fueran compatibles con el PSP y PSvita

Abriendo el proyecto.

Lo primero que debemos de hacer es descargar el proyecto (aquí) y abrirlo con #Develop o visual studio. Para poder usrlos, primero debemos de agregar un par de referencias a los siguientes ensamblados: MediainfoNet.dll y ListviewEx.dll (incluidas en la descarga). La primer librería solo se trata de un "wrapper" para poder usar la librería MediaInfo.dll que sirve para obtener la información detallada de los archivos multimedia (Resolucion, BitRate, etc) y la segunda es una versión extendida del ListView, posee algunas funciones extra a un ListView normal, estas las veremos mas adelante.
Al inicio, puede aparecer este mensaje de error.
Para agregar las referencias solo hay que ir a "Proyecto>Agregar referencia" y en la ficha "Examinar", buscar la carpeta debug que viene incluida en el archivo que descargaste y seleccionar "ListViewEx.dll y MediaInfoNet.dll"


Analizando un poco el código.

El código está dividido en dos regiones:
  1. Declaraciones (globales)
  2. Funciones
El resto del código que no está dentro de ninguna de estas, es código propio del formulario o de los controles; eventos constructores y todo lo demás no tienen ninguna Region definida.

Para VEncoder2 solo usaremos 4 variables globales

    #region Declaraciones
       private bool IsConvertingSomething = false;
       private bool RequestForCancel = false;
       private int ElapsedTimeGlobal = 0;
       private int ElapsedTimeCurrent = 0;
    #endregion

Los nombres son realmene explicitos, entonces creo que no hace falta mencionar cada una, de todos modos sobre la marcha, veremos que tipo de valores se le asignan a cada una.

Función SaveProfile

Para no hacer muy largo el post,el día de hoy solo analizaremos las primeras dos funciones que nos encontraremos al abrir el proyecto. La primera funcion es la que se encarga de abrir un archivo VEPX con los ajustes para la conversion.


/// <summary>
/// Funcion que se encarga de guardar loa ajustes en archivo VEPX
/// </summary>
/// <param name="XmlPath">Ruta en donde se guardara</param>
/// <param name="ProfileName">Nombre que se usara para identificar el archivo</param>

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();
  }
}


Lo que va a hacer es leer los controles y guardar sus valores en un archivo XML, para eso, usamos XMLWritter. De hecho lo hacemos de forma inpractica, pero debido a que siempre van a ser los valores de los mismos controles, ademas de que no son muchos, entonces podemos hacer de forma explicita, indicando el nombre del control cuyo valor queremos obtener y simplemente guardarlo. Por ejemplo,  XWriter.WriteElementString("WH", CboResolucion.Text); lo que hace es guardar en un elemento XML llamado "WH" el texto contenido en CboResolution. Para poder entender un poco mejor a que se hace referencia, recomiendo echarle un vistazo al proyecto, cada control tiene su nombre, entonces una vez que te familiarices con los nombres, sera más rápido entender el código.

Funcion LoadProfile

La siguiente función hace lo contrario, en lugar de guardar, está lee desde un archivo existente.
/// <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))
 {
 XMLR.ReadToFollowing("Name");
 lnklblLoadProfile.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();
 }
}


Verás que el código es realmente similar al anterior, de hecho lo es, pero para poder entenderlo es necesario estar familiarizado con los nombres de los controles.

Creo que por ahora es todo, sirve que le echas in vistazo al código y ya despues seguiremos con la explicación a lo demás.

Los leo luego.