Xwork's Blog

The lord is waiting to take your hand.

Vamos a programar #77 - VEncoder 2.0.1

No hay comentarios.
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.

No hay comentarios. :

Publicar un comentario

Vamos a programar #76 - Neo Clock View.

No hay comentarios.
Hola de nuevo a todos, el día de hoy vamos a ver un poco de algo a lo que decidí llamar "Neo Clock View". Para fabricarlo vamos a hacer uso de los displays de siete segmentos que vimos en el post anterior.

En la versiones anteriores de Clock View, usamos el módulo DS1302. En esta ocasión haremos uso del modulo DS1307 y haremos una versión solo para probar si los "displays" funcionan cómo esperamos.

Antes de comenzar, deberás de instalar la librería para poder usar el modulo de reloj de forma sencilla. La puedes descargar del GitHub del dueño.

Una vez instalada, vamos a realizar las conexiones. Para esto simplemente conectamos de la siguiente manera.

  • SDA - Al pin A4 del arduino.
  • SCL - Al pin A5 del arduino.
  • VCC - A 5v del arduino.
  • GND - A GND del arduino.
Con las conexiones hechas ahora revisemos el código.

//Reloj
// A4 - SDA
// A5 - SCL
#include <FastLED.h>
#define NUM_LEDS 126
#define DATA_PIN 9
#include <Wire.h>
#include <TimeLib.h>
#include <DS1307RTC.h>

CRGB leds[NUM_LEDS];
int XC = 0;

void setup() {
	// Serial.begin(9600);
	// while (!Serial) ;
	// delay(200);
	LEDS.addLeds<WS2812,DATA_PIN,RGB>(leds,NUM_LEDS);
	LEDS.setBrightness(10);
}

void PrintNumber(int Value, int Digit, int Green, int Red, int Blue){
	for(int i = 0; i < 7; i++) {
		switch (Value)
		{
			case 0:
				if (i == 6){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0, 0, 0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0, 0, 0);
					leds[i * 3 + (Digit * 21)].setRGB(0, 0, 0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i*3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i*3+ (Digit * 21)].setRGB(Green, Red,Blue);		
				}
				FastLED.show(); 
				break;
			case 1:
				if (i == 0 || i == 1){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);			
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i*3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}
			FastLED.show();
			break;
			case 2:
				if (i == 0 || i == 3){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 3:
				if (i == 3 || i == 4){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 4:		
				if (i == 2 || i == 4 || i == 5){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 5:
				if (i == 1 || i == 4){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 6:
				if (i == 1){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 7:
				if (i == 3 || i == 6 || i == 4 || i == 5){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3)+ 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3)+ 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			case 8:
				leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
				leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
				leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
			FastLED.show();
			break;
			case 9:
				if (i == 4){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
			default:		
				if (i == 0 || i == 1){
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(0,0,0);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(0,0,0);
					leds[i * 3 + (Digit * 21)].setRGB(0,0,0);
				}else{
					leds[(i * 3) + 2 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[(i * 3) + 1 + (Digit * 21)].setRGB(Green, Red,Blue);
					leds[i * 3 + (Digit * 21)].setRGB(Green, Red,Blue);
				}
			FastLED.show();
			break;
		}
	}
}

void ShowIntro(){
	for(int i = 0; i < NUM_LEDS; i++) {
		leds[i] = CHSV(255, 255, 255);
		FastLED.show(); 
		delay(5);
	}
	Intro = false;
}

void PrintTime(int Hour, int Minutes, int Seconds){
	if(Seconds < 10){
		PrintNumber(Seconds,0,255,0,0);
		PrintNumber(0,1,255,0,0);
	}else{
		PrintNumber(Seconds % 10, 0, 255, 0, 0);
		PrintNumber(Seconds / 10, 1, 255, 0, 0);
	}
	
	if(Minutes < 10){
		PrintNumber(Minutes,2,0,255,0);
		PrintNumber(0,3,0,255,0);
	}else{
		PrintNumber(Minutes % 10, 2, 0, 255, 0);
		PrintNumber(Minutes / 10, 3,0 , 255, 0);
	}
	
	if(Hour < 10){
		PrintNumber(Hour,4,0,0,255);
		PrintNumber(0,5,255,0,0);
	}else{
		PrintNumber(Hour % 10, 4, 0, 0,255);
		PrintNumber(Hour / 10, 5, 0, 0,255);
	}
}

tmElements_t tm;
bool Intro = true;

void loop() {
	
	if (Intro == true) ShowIntro();

	if (RTC.read(tm)) {
		PrintTime(tm.Hour,tm.Minute,tm.Second);
	} else {
		if (RTC.chipPresent()) {
			PrintTime(1025,1024,1023);
			// Serial.println("The DS1307 is stopped.  Please run the SetTime");
			// Serial.println("example to initialize the time and begin running.");
			// Serial.println();
		} else {
			// Serial.println("DS1307 read error!  Please check the circuitry.");
			// Serial.println();
		}
		delay(9000);
	}
	delay(100);
}

El código consta de tres funciones. La primera de ellas en orden de aparicion es "PrintNumber", hay que recordar que diseñamos los segmentos de forma tal; que cada tres LED's representan un segmento. La función recibe cinco parámetros. El primero es el valor que el digito tendrá y técnicamente puede ser cualquier valor (dentro del tipo "integer"), pero debido a que solo podemos mostrar valores dentro del rango 0 ~ 9, deberíamos pasar valores que no excedan nueve. El siguiente parámetro debe de ser un valor que nos indicara que digito es el que queremos usar, dado que empezamos de derecha a izquierda, si decimos que queremos usar el digito uno, estaríamos usando el del lado derecho.
Los ultimos tres parametrosn, son los valores que se usaran para cada color empezando por el rojo, luego el verde y finalmente el azul. El valor para cada uno de estos deberá de estar entre los valores 0 ~ 255, y mezclandolos, podemos crear cualquier color.

la siguiente función es "ShowIntro()", esta función sirve para hacer una animación que recorrerá cada un de los LED de inicio a fin, solo se hará una vez y sirve para demostrar de que otra forma se pueden controlar los LED's.

La siguiente función es "PrintTime()". Recibe tres parámetros del tipo "int", el primero es el valor que representan las horas, el segundo es un valor que representa a los minutos y finalmente el tercero es un valor que representa a los segundos. dentro de la función se usan tres condiciones; la primera para los segundos; cuando se le hace la consulta del tiempo al modulo de reloj, si el tiempo es menor a diez segundos, este devolverá un solo dígito; es decir, si el tiempo tiene solo siete segundos en lugar de devolver "07" que seria el valor que esperamos, solo nos regresaría "7". La comprobación sirve para cuando sucede este caso. Para solucionarlo, si el tiempo (segundos, minutos u horas) es menor a 10, llamamos a la función "PrintTime()" dos veces y en la primera llamada, le pasamos como primer parámetro el valor de los segundos y en el segundo parámetro el valor "0" que indica que queremos usar la primer matriz (el orden ya lo había dicho). Para la segunda llamada, le pasaremos el valor de "0" cómo primer parámetro y "1" cómo segundo, así cuando pase el caso de siete segundos (por ejemplo) tendremos un número para mostrar en el segundo display. Cuando es el caso contrario o mejor dicho cuando los valores son mayores o iguales a diez, lo único que hacemos dividir y en la primera llamada pasamos el resultado del modulo del valor actual (segundos, minutos u horas) y diez. Para la segunda llamada simplemente dividimos en valor entre diez y esto nos dará el valor que debemos de mostrar en la segunda llamada.

Dentro del bucle principal lo primero que hacemos es llamar a la función "ShowIntro" valiendonos de un flag llamado "Intro" que se inicializa en "true", la condición dice que cuando esta variable es verdadero se hace la animación, al se una variable global, cuando la animación acaba, cambia el valor de la variable "Intro" a "false" por lo que la animación solo se produce una sola vez. Acabada la animación, usamos las funciones de la libreria del reloj. Primero comprobamos si se puede leer desde el módulo, si es así llamamos a la función "PrintTime()" con las horas; "tm.Hour", los minutos; "tm.Minute", y los segundos "tm.Seconds". Con esto mostramos la hora en los "displays". Si no se puede obtener la hora del módulo, se trata de comprobar si el chip está disponible, si es así solamente bastará con subir el ejemplo que viene en la librería (disponible en Ejemplos >> DS1307RTC >> SetTime). Solo tendremos que subirlo antes que este código y ya tendrá la hora ajustada.

Y bien, por ahora es todo, en el próximo post veremos cómo mejorar un poco más el reloj, aunque creo que lo ideal seria usar el código de clock view y solamente adaptar las partes que necesitemos.

Los leo luego.

No hay comentarios. :

Publicar un comentario