Insertándome en el mundo Python, descubro cosas muy interesantes, una de ellas es la facilidad de uso que tienen muchas de las librerías de mi Linux.
En este caso, me metí un poco con el GStreamer, un fabuloso manejador de archivos multimedia. Encontré un ejemplo de un reproductor de videos, me pregunté qué le faltaba para que tenga lo que yo necesito y empecé a modificarlo.
El ejemplo original es del tutorial oficial de pygst y se encuentra en: http://pygstdocs.berlios.de/pygst-tutorial/playbin.html, en el ejemplo 2.2. Es un reproductor simple, con un cuadro de texto y un botón “Play”, hay que ingresar la ruta completa al archivo de video y luego darle play para que funcione.
Esto tiene muchísimas limitaciones y muy poca funcionalidad, entonces decidí modificarlo a mi gusto, para que quede como yo lo necesitaba. Así fue que me quedó un reproductor con las siguientes características:
- Reproductor multimedia de 7.2K de peso, hecho con Python, GTK y GStreamer (en Ubuntu necesita los paquetes python, python-gtk2 y python-gst0.10)
- Soporta todos los tipos de archivo que soporte GStreamer (según los plugins que se tengan instalados)
- Reproduce los subtítulos del video que tengan el mismo nombre de archivo y extensión .srt
- Reproduce el archivo que le paso como parámetro por la linea de comandos, únicamente. Esto me permite asociarlo al tipo de archivo que yo quiera en el Nautilus. No necesito otra forma de abrir los archivos.
- Tiene un botón de Play/Pausa, uno de Avance y otro de Retroceso. Estos dos últimos suman y restan 60 y 30 segundos (respectivamente) al tiempo de reproducción. La diferencia entre el avance y el retroceso es intencional.
- Los botones toman los iconos standard del tema que se esté usando en GTK.
- Informa del tiempo de reproducción actual y del tiempo total del video.
- Tiene una barra de tiempo deslizante, que se actualiza con el transcurso del video y puede adelantar/retrasar el mismo.
- Inicia la reproducción automáticamente al correr el programa.
- Soporte un OSD (desactivado por default) que muestra el tiempo en la esquina superior-izquierda del video.
- Soporte algunas combinaciones de teclas:
- Tecla P: Reproduce o pausa la reproducción.
- Tecla F: Cambia al modo pantalla completa.
- Flecha Izquierda: Retrocede 30 segundos la reproducción.
- Shift + Flecha Izquierda: Retrocede 60 segundos la reproducción.
- Flecha Derecha: Avanza 60 segundos la reproducción.
- Shift + Flecha Derecha: Avanza 120 segundos la reproducción.
Hay muchos valores que están “hardcoded”, como la fuente a usar para los subtítulos, la opción de mostrar los subtítulos, la opción de mostrar el osd y la de reproducir al inicio, que se podrían poner en un archivo rc o algo así, pero, por ahora, funciona bien para mi gusto.
Descargar el archivo: microplayer.py.tar
Para usarlo, hay que descomprimir, dar permisos de ejecución al archivo .py y después correrlo con el nombre de archivo a reproducir. Los comandos son los siguientes:
tar -xzf microplayer.py_.tar.gz chmod +x microplayer.py ./microplayer.py /home/usuario/Videos/mipelicula.avi
Acá está el código:
#!/usr/bin/env python
import sys, os, thread, time
import pygtk, gtk, gobject
import pygst
pygst.require("0.10")
import gst
class GTK_Main:
SHOW_OSD = False
SHOW_SUBTITLES = True
SUBTITLES_FONT = 'sans bold 18'
PLAY_ON_STARTUP = True
PLAY_IMAGE = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)
PAUSE_IMAGE = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)
PREV_IMAGE = gtk.image_new_from_stock(gtk.STOCK_MEDIA_REWIND, gtk.ICON_SIZE_BUTTON)
NEXT_IMAGE = gtk.image_new_from_stock(gtk.STOCK_MEDIA_FORWARD, gtk.ICON_SIZE_BUTTON)
def __init__(self):
self.player = gst.element_factory_make("playbin2", "player")
bus = self.player.get_bus()
bus.add_signal_watch()
bus.enable_sync_message_emission()
bus.connect("message", self.on_message)
bus.connect("sync-message::element", self.on_sync_message)
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
self.player.set_property("uri", "file://" + sys.argv[1])
if self.SHOW_SUBTITLES:
suburi = sys.argv[1][:sys.argv[1].rfind('.')] + '.srt'
if os.path.isfile(suburi):
self.player.set_property("suburi", "file://" + suburi)
self.player.set_property("subtitle-font-desc", self.SUBTITLES_FONT)
else:
print "Info: el archivo no tiene subtitulos.\n"
else:
print "No se ha especificado un archivo a reproducir o el mismo no puede leerse. Verifique la ruta y los permisos."
exit(1)
if self.SHOW_OSD:
bin = gst.Bin("my-bin")
timeoverlay = gst.element_factory_make("timeoverlay")
bin.add(timeoverlay)
pad = timeoverlay.get_pad("video_sink")
ghostpad = gst.GhostPad("sink", pad)
bin.add_pad(ghostpad)
videosink = gst.element_factory_make("autovideosink")
bin.add(videosink)
gst.element_link_many(timeoverlay, videosink)
self.player.set_property("video-sink", bin)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_title("Micro Video Player")
window.set_default_size(500, 400)
window.connect("destroy", self.on_quit, "WM destroy")
window.connect('key_press_event', self.on_key_press)
window.unfullscreen()
self.fullscreen = False
self.window = window
vbox = gtk.VBox()
window.add(vbox)
self.movie_window = gtk.DrawingArea()
vbox.add(self.movie_window)
hbox = gtk.HBox()
self.controls = hbox
self.btn_rewind = gtk.Button()
self.btn_rewind.set_image(self.PREV_IMAGE)
self.btn_rewind.connect("clicked", self.rewind, 30)
hbox.pack_start(self.btn_rewind, False)
self.btn_start_stop = gtk.Button()
self.btn_start_stop.set_image(self.PLAY_IMAGE)
self.btn_start_stop.connect("clicked", self.start_stop)
hbox.pack_start(self.btn_start_stop, False)
self.btn_forward = gtk.Button()
self.btn_forward.set_image(self.NEXT_IMAGE)
self.btn_forward.connect("clicked", self.forward, 60)
hbox.pack_start(self.btn_forward, False)
self.seek = gtk.HScale();
self.seek.set_draw_value(False)
self.seek.connect('value-changed', self.on_slider_change)
hbox.pack_start(self.seek, True)
self.time_label = gtk.Label()
self.time_label.set_text('0:00:00 / 0:00:00')
hbox.pack_end(self.time_label, False)
vbox.pack_end(hbox, False)
window.show_all()
self.is_playing = False
if self.PLAY_ON_STARTUP:
self.start_stop(window)
def start_stop(self, w):
if self.is_playing:
self.play_thread_id = None
self.player.set_state(gst.STATE_PAUSED)
self.btn_start_stop.set_image(self.PLAY_IMAGE)
self.is_playing = False
else:
self.btn_start_stop.set_image(self.PAUSE_IMAGE)
self.player.set_state(gst.STATE_PLAYING)
self.play_thread_id = thread.start_new_thread(self.play_thread, ())
self.is_playing = True
def play_thread(self):
play_thread_id = self.play_thread_id
gtk.gdk.threads_enter()
self.time_label.set_text("0:00:00 / 0:00:00")
gtk.gdk.threads_leave()
while play_thread_id == self.play_thread_id:
try:
time.sleep(0.2)
dur_int = self.player.query_duration(gst.FORMAT_TIME, None)[0]
if dur_int == -1:
continue
dur_str = self.convert_ns(dur_int)
gtk.gdk.threads_enter()
self.seek.set_adjustment(gtk.Adjustment(0, 0, dur_int, 30 * 1000000000, 60 * 1000000000, 0));
self.time_label.set_text("0:00:00 / " + dur_str)
gtk.gdk.threads_leave()
break
except:
pass
time.sleep(0.2)
while play_thread_id == self.play_thread_id:
try:
pos_int = self.player.query_position(gst.FORMAT_TIME, None)[0]
pos_str = self.convert_ns(pos_int)
if play_thread_id == self.play_thread_id:
gtk.gdk.threads_enter()
self.time_label.set_text(pos_str + " / " + dur_str)
self.seek.handler_block_by_func(self.on_slider_change)
self.seek.set_value(pos_int)
self.seek.handler_unblock_by_func(self.on_slider_change)
gtk.gdk.threads_leave()
except:
pass
time.sleep(0.5)
def on_slider_change(self, slider):
seek_ns = self.seek.get_value()
self.player.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
def rewind(self,w,time):
pos_int = self.player.query_position(gst.FORMAT_TIME, None)[0]
seek_ns = pos_int - (time * 1000000000)
if seek_ns < 0: seek_ns = 0 self.player.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) def forward(self,w,time): pos_int = self.player.query_position(gst.FORMAT_TIME, None)[0] seek_ns = pos_int + (time * 1000000000) if seek_ns > self.player.query_duration(gst.FORMAT_TIME, None)[0]:
seek_ns = self.player.query_duration(gst.FORMAT_TIME, None)[0]
self.player.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
def on_quit(self,w,m):
self.player.set_state(gst.STATE_NULL)
self.is_playing = False
gtk.main_quit()
def on_message(self, bus, message):
t = message.type
if t == gst.MESSAGE_EOS:
self.player.set_state(gst.STATE_NULL)
self.btn_start_stop.set_image(self.PLAY_IMAGE)
elif t == gst.MESSAGE_ERROR:
self.player.set_state(gst.STATE_NULL)
err, debug = message.parse_error()
print "Error: %s" % err, debug
self.btn_start_stop.set_image(self.PLAY_IMAGE)
def on_sync_message(self, bus, message):
if message.structure is None:
return
message_name = message.structure.get_name()
if message_name == "prepare-xwindow-id":
imagesink = message.src
imagesink.set_property("force-aspect-ratio", True)
gtk.gdk.threads_enter()
imagesink.set_xwindow_id(self.movie_window.window.xid)
gtk.gdk.threads_leave()
def convert_ns(self, t):
# This method was submitted by Sam Mason.
# It's much shorter than the original one.
s,ns = divmod(t, 1000000000)
m,s = divmod(s, 60)
if m < 60:
return "0:%02i:%02i" %(m,s)
else:
h,m = divmod(m, 60)
return "%i:%02i:%02i" %(h,m,s)
def on_key_press(self, w, event):
# Cambia el delay del avance/retroceso:
# si se presiona SHIFT, se avanza/retrocede 120segs, sino 60segs
if event.state & gtk.gdk.SHIFT_MASK:
delay = 120
else:
delay = 60
val = event.keyval
if val == 102 or val == 70: # Letra F - Fullscreen
if self.fullscreen:
self.controls.show()
self.window.unfullscreen()
self.fullscreen = False
else:
self.controls.hide()
self.window.fullscreen()
self.fullscreen = True
if val == 112 or val == 80: # Letra P - Play/Pausa
self.start_stop(w)
if val == 65361: # Flecha Izq - Retroceder
self.rewind(w,delay / 2)
if val == 65363: # Flecha Der - Avanzar
self.forward(w,delay)
#print "Codigo [%d] presionado" % (val)
GTK_Main()
gtk.gdk.threads_init()
gtk.main()

