Micro Reproductor de Videos en Python


Captura MicroPlayer

Captura MicroPlayer

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:
    1. Tecla P: Reproduce o pausa la reproducción.
    2. Tecla F: Cambia al modo pantalla completa.
    3. Flecha Izquierda: Retrocede 30 segundos la reproducción.
    4. Shift + Flecha Izquierda: Retrocede 60 segundos la reproducción.
    5. Flecha Derecha: Avanza 60 segundos la reproducción.
    6. 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()

, , , ,

  1. No hay Comentarios
(No será publicado)