Out Of Bounds Exception!

Mi lugar de irreflexión y algo de programación

El lío con Java y Bash

Me considero un Windowsero (no confundir con fanboy) de toda la vida, de hecho hasta hace apenas uno o dos años no sabía ni que existía Linux (qué le vamos a hacer) y cuando me lo presentaron obviamente no me gustaba: una interfaz “cutrilla”, mucho que teclear en la consola, dificultad para instalar/desinstalar cosas, pocos juegos (xD) y todas esas cosillas que un usuario de Windows dice (que no tienen por qué ser ciertas).

No obstante, una vez que le fui cogiendo el truco, me ha llegado a gustar y, a pesar de que no lo uso como SO principal, sí lo hago para trabajar, hacer el PFC o para trastear con temas de seguridad, scripting, etc. Es cuestión de gustos, para unas cosas me es más útil Windows y para otras Linux =) (no me voy a meter en para qué cosas es mejor uno u otro, cada cual que les saque el partido que quiera/pueda).

El caso es que últimamente estoy trasteando en un servidor Ubuntu con Tomcat (a.k.a. Servlets en Java) al que accedo mediante Kitty (un fork del famoso Putty), desde donde me dedico principalmente a toquetear una base de datos Mysql y a mirar algunos logs, nada del otro mundo.

Hace poco sacamos una nueva tarea en el scrum del proyecto que consistía en, dado un fichero multimedia (audio o vídeo), sacar la duración del mismo en el servidor. Como ya se usa ffmpeg en otra parte del servidor, decidí ver qué tal se haría así en vez de mediante Java (ya que para ello tengo entendido que se requieren librerías externas). Por tanto mi primera tarea era ver cómo me daba este valor el programa, lo cual es bastante simple (lo voy a mostrar paso a paso de cómo lo he ido haciendo, por lo que es normal que algún comando del principio no funcione xD):

ffmpeg -i video.3gp

Lo cual nos devuelve este tochaco:

ffmpeg version 0.8.git, Copyright (c) 2000-2011 the FFmpeg developers
  built on Jul 21 2011 08:29:21 with gcc 4.3.2
  configuration: --disable-yasm
  libavutil    51. 11. 0 / 51. 11. 0
  libavcodec   53.  8. 0 / 53.  8. 0
  libavformat  53.  6. 0 / 53.  6. 0
  libavdevice  53.  2. 0 / 53.  2. 0
  libavfilter   2. 27. 0 /  2. 27. 0
  libswscale    2.  0. 0 /  2.  0. 0
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x9ec4380] max_analyze_duration 5000000 reached at 5046000
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'ruta/video.3gp':
  Metadata:
    major_brand     : 3gp4
    minor_version   : 768
    compatible_brands: 3gp4mp413gp6
    copyright       :
    copyright-eng   :
  Duration: 00:00:05.28, start: 0.000000, bitrate: 91 kb/s
    Stream #0.0(eng): Video: mpeg4, yuv420p, 176x144 [PAR 1:1 DAR 11:9], 74 kb/s, 19.10 fps, 30 tbr, 1k tbn, 30 tbc
    Stream #0.1(eng): Audio: amrnb, 8000 Hz, 1 channels, flt, 12 kb/s
    Stream #0.2(eng): Data: mp4s / 0x7334706D, 0 kb/s
    Stream #0.3(eng): Data: mp4s / 0x7334706D, 0 kb/s
At least one output file must be specified

Donde se puede observar también que nos da un pequeño error en la última línea, dado que no le hemos especificado el archivo de salida ya que no nos interesa al querer extraer su información (si no, nos sobrescribiría el fichero o crearía un archivo igual que luego tendríamos que borrar). Pero bueno, al menos tenemos la duración, así que sólo falta extraerla del tochaco ese, por lo que vamos probando con algo simple:

ffmpeg -i video.3gp | grep Duration

Pero la sorpresa está en que no ocurre nada, vuelve a salir todo el chorrazo otra vez… Después de un rato devanándome los sesos descubro que el problema está en que la salida la hace por stderr, por lo que el pipe no lo coge como entrada, así que Googleando un poco encuentro un arreglo:

ffmpeg -i video.3gp 2>&1 | grep Duration

Que básicamente redirige la salida de error (2>) a la salida estándar (&1). Y, ahora sí, nos devuelve la línea en cuestión:

  Duration: 00:00:05.28, start: 0.000000, bitrate: 91 kb/s

Continuando un poco y encadenando algún comando más nos sale algo como esto:

ffmpeg -i video.3gp 2>&1 | grep Duration | awk '{print $2}' | sed 's/.\{4\}$//'

Que básicamente lo que hace es coger el segundo campo con awk y quitar los 4 últimos caracteres correspondientes al punto, los centisegundos y la coma separadora.

Si se prefiere no usar tantos pipes, con awk se llega a la misma solución (aunque se podría mejorar la expresión regular a algo como esto (\d\d:){2}\d\d, suponiendo que soporte el \d para dígitos y el {N} para repeticiones):

ffmpeg -i $1 2>&1 | awk '/Duration: [0-9][0-9]:[0-9][0-9]:[0-9[0-9]/ {print substr($2,1,8)}'

Pues qué bonito todo, la vida nos sonríe y esto ya está (o no ;)), sólo falta metérselo a Java para que obre su magia, básicamente algo así:

String command = "ffmpeg -i " + file.getAbsolutePath() + " 2>&1 | grep Duration | awk '{print $2}' | sed 's/.\\{4\\}$//'";
Process pr = Runtime.getRuntime().exec(command);

Posteriormente habría que coger el InputStream de nuestro objeto pr y leer de ahí (sí, es InputStream y no OutputStream, es que son unos cachondos =])

El problema (porque obviamente iba a haber más de uno) es que esto no funciona y el por qué, ahora obvio para mí después de pelearme con ello, es el siguiente: al llamar al método exec(comando) estamos ejecutando el programa del comando en cuestión (ffmpeg en este caso) y, dado que los pipes son propios de Linux y no de este programa, obviamente esto no puede funcionar. Por tanto una aproximación más válida sería algo como esto:

"bash -c \"ffmpeg -i " + file.getAbsolutePath() + " 2>&1 | grep Duration | awk '{print $2}' | sed 's/.\\{4\\}$//'\"";

Que básicamente ejecuta una shell con esos comandos donde los pipes si estarían permitidos. Aún así, nos suelta un error más:

-i: -c: line 0: unexpected EOF while looking for matching `"'
-i: -c: line 1: syntax error: unexpected end of file

Que probablemente se deba al lío de comillas, barras normales e invertidas y demás que tenemos ahí… Con lo cual aquí se me iluminó la bombilla y decidí que iba a optar por algo mejor: un script. Me parece una solución mucho mejor por varios motivos:

  • Es optimizable: si nuestro ejecutable (ffmpeg) se actualiza y hay que cambiar los comandos, encontramos otro mejor o símplemente vemos una solución más óptima basta con modificar el script sin modificar la entrada o salida de datos.
  • Es reutilizable: Puede que nos sirva para un uso particular o para otros proyectos más adelante.
  • Es cómodo: no tendremos que estar cogiendo el código del servidor y ejecutándolo para probar cosas, ya que basta con ejecutar el script y voilá. Asimismo por cada modificación no hay que estar haciendo deploy del servidor y demás…

La “desventaja” que veo sería la portabilidad, ya que habría que mover el script con el servidor en caso de que se despliegue en otro sitio. Además habría que vigilar la posibilidad de que fuese eliminado, de modo que se volviese a generar o al menos que no causase alguna excepción graciosa.

Y bueno, hasta aquí mi experiencia de estos días con Linux y Java, al final lo hice funcionar (a pesar de subestimar la tarea) y me he quedado a gusto =P

Agur!

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: