''' SVPlayer player backend objects (c) 2012-2015 Jan ONDREJ (SAL) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. https://live.gnome.org/PyGObject/ https://wiki.ubuntu.com/Novacut/GStreamer1.0 gst-launch examples: gst-launch udpsrc multicast-group=233.10.47.13 port=1234 skip-first-bytes=8 ! queue ! mpegtsparse ! tsdemux ! mpeg2dec ! queue ! ffdeinterlace ! xvimagesink gst-launch filesrc location=/tmp/test.mpg ! queue ! mpegtsparse ! tsdemux name=s s. ! queue ! mad ! pulsesink s. ! mpeg2dec ! queue ! ffdeinterlace ! xvimagesink gst-launch udpsrc skip-first-bytes=0 multicast-group=232.232.64.18 port=5004 ! queue ! mpegtsparse ! decodebin2 name=s async-handling=true s. ! pulsesink s. ! autovideosink gst-launch dvbbasebin frequency=778000000 bandwidth=8 guard=3 modulation=3 trans-mode=1 hierarchy=0 program-numbers=2001 ! queue ! decodebin2 name=s s. ! volume ! pulsesink s. ! autovideosink gst-launch-1.0 rtspsrc location=rtsp://$USER:$PW@cam/video.mjpg name=s s. ! queue ! decodebin ! autovideosink s. ! queue ! decodebin ! autoaudiosink Install gstreamer1 for Fedora 17: wget --no-check -O /etc/yum.repos.d/salstar.repo \ https://www.salstar.sk/pub/salpack/etc/yum.repos.d/salstar.repo yum --enablerepo=salstar.sk-test install gstreamer1\* \ --exclude=\*devel\* --exclude=\*debuginfo ''' import sys, os, time, glob, urlparse from urllib import pathname2url from datetime import datetime from paths import recorder_template from sgtk import * from config import subtitle_font, subtitle_encoding from base import BasePlayerWidget, Debug # Import gstreamer libraries import gi gi.require_version('Gst', '1.0') gi.require_version('GstVideo', '1.0') gi.require_version('GstMpegts', '1.0') from gi.repository import Gst, GstVideo # required for embedding from gi.repository import GstMpegts # MPEG structures Gst.init(None) ### GStreamer Player ### nanosecond = 10L**9 # nanoseconds of 1 second percent_scale = 10000 GST_PLAY_FLAG_TEXT = 4 class attr2dict: def __init__(self, obj): self.obj = obj def __getitem__(self, key): if key=='uri': return self.obj.geturi() a = getattr(self.obj, key) if callable(a): return a() return a def gstdatetime(dt): return datetime( year = dt.get_year(), month = dt.get_month(), day = dt.get_day(), hour = dt.get_hour(), minute = dt.get_minute(), second = dt.get_second() ) class GST_cmd: def pl(s, kw={}): ''' Gstreamer player with parameters. ''' return [ " ! ".join([x.strip() for x in s.strip().split("\n")]), kw ] # parameter conversion table to convert VLC parameters to GST parameters _parameter_conversion = { "frequency": int, "inversion": lambda x: { 0:0, 1:1, -1:2, "0":0, "1":1, "-1":2 }.get(x, x), "bandwidth": lambda x: {8:0, 7:1, 6:2, -1:3}.get(int(x), x), "guard": lambda x: { 32:0, 16:1, 8:2, 4:3, -1:4, "1":0, "1/4":2 }.get(x, x), "hierarchy": lambda x: {0:0, 1:1, 2:2, 4:3, -1:4}.get(int(x), x), "modulation": lambda x: { 16:1, 32:2, 64: 3, 128:4, 256:5, -1:6, "16QAM":1, "32QAM":2, "64QAM":3, "128QAM":4, "256QAM":5, "AUTO":6 }.get(x, x), "transmission": lambda x: {2:0, 8:1, -1:2}.get(int(x), x), "code-rate-hp": int, "code-rate-lp": int, "program": int } prefer_vaapi = os.environ.get("SVPLAYER_VAAPI", "").split(",") def __init__(self, uri, subtitles=[], options={}): self.uri = uri self.subtitle_files = ["file://"+pathname2url(x) for x in subtitles] if uri is None: self.scheme = "file" elif type(uri)==int: self.scheme = 'fd' elif "://" in uri: if uri.startswith("rtp://"): uri = "udp" + uri[3:] self.uri = uri self.scheme, rest = uri.split("://", 1) else: self.scheme = "file" self.uri = os.path.abspath(uri) # Enable/Disabled vaapi (hw acceleration) for va_element in ["decode", "sink"]: vaapi = Gst.ElementFactory.find("vaapi"+va_element) if vaapi: vaapi.set_rank( (va_element in self.prefer_vaapi) and Gst.Rank.PRIMARY or Gst.Rank.MARGINAL ) print "VAAPI rank:", va_element, vaapi.get_rank() self.player = Gst.Pipeline() self.playbin = Gst.ElementFactory.make("playbin", "src") self.playbin.connect("source-setup", self.source_setup) self.playbin.set_property("message-forward", True) # following option will make forwarding crazy #self.playbin.set_property("async-handling", True) self.playbin.set_property("flags", 0x001 # render video | 0x002 # render audio | 0x004 * int(bool(self.subtitle_files)) # render subtitles | 0x008 # audio visualization #| 0x010 # soft volume | 0x080 # progressive download buffering # buffering will break playing of large mkv files #| 0x100 # buffering | 0x200 # deinterlace #| 0x400 # soft-colorbalance ) #self.playbin.set_property("buffer-size", 100*1048576) # 100 MB #self.playbin.set_property("buffer-duration", 10*nanosecond) # 10s #self.playbin.set_property("ring-buffer-max-size", 10485760) # 10 MB if subtitle_font: self.playbin.set_property("subtitle-font-desc", subtitle_font) if subtitle_encoding: self.playbin.set_property("subtitle-encoding", subtitle_encoding) if self.subtitle_files: print "Setting suburi:", self.subtitle_files self.playbin.set_property("suburi", self.subtitle_files[0]) if options.get("audio_only"): self.update_flags(-0x008) # no audio visualization self.player.add(self.playbin) # video filter self.video = self.playbin self.deinterlacer = None # audio filter self.volume = self.playbin def relink_deinterlace(self): # relink deinterlace - broken code self.player.set_state(Gst.State.PAUSED) time.sleep(1) old_deinterlace = self.player.get_by_name("deinterlace") if old_deinterlace: new_deinterlace = \ Gst.ElementFactory.make("avdeinterlace", "deinterlace") parent = old_deinterlace.parent parent.remove(old_deinterlace) parent.add(new_deinterlace) self.player.get_by_name("vdbin").link(new_deinterlace) new_deinterlace.link(self.player.get_by_name("vdconv")) else: print "NO DEINTERLACE YET!" self.player.set_state(Gst.State.PLAYING) def source_setup(self, playbin, src): protocols = src.get_protocols() if 'udp' in protocols: print "SOURCE SETUP:", src, "multicast" #src.set_property("multicast-iface", "eth0") src.set_property("caps", Gst.caps_from_string( "application/x-rtp, media=(string)video, clock-rate=(int)90000, " "encoding-name=(string)MP2T-ES, payload=(int)33" )) elif ('http' in protocols or 'https' in protocols) \ and 'cookies' in self.options: print "SOURCE SETUP:", src, "http(s)" src.set_property("cookies", self.options['cookies']) def set_uri(self, uri, options={}): self.options = options if type(uri)==int: uri = "fd://%d" % uri elif "://" not in uri: uri = "file://" + pathname2url(os.path.abspath(uri)) elif uri.startswith("rtp://"): uri = "udp" + uri[3:] print "Setting URI:", uri self.playbin.set_property("uri", uri) def get_flag(self, flag): return (int(self.playbin.get_property("flags")) & flag)>0 def update_flags(self, flag): old_flags = int(self.playbin.get_property("flags")) if flag<0: flags = old_flags & (0xffff + flag) else: flags = old_flags | flag if old_flags==flags: return False print "Changing flags:", old_flags, flags self.playbin.set_property("flags", flags) return True def set_track(self, t="audio", n=-1): if self.playbin.get_property("current-%s" % t)!=n: print "set_%s_track %s" % (t, n) self.player.set_state(Gst.State.PAUSED) self.playbin.set_property("current-%s" % t, n) self.player.set_state(Gst.State.PLAYING) def get_tracks(self, t="audio"): track = self.playbin.get_property("current-%s" % t) n_tracks = self.playbin.get_property("n-%s" % t) tracks = [] n = 0 for i in range(n_tracks): tags = self.playbin.emit("get-%s-tags" % t, i) if tags is None: continue code = tags.get_string(Gst.TAG_LANGUAGE_CODE) name = tags.get_string(Gst.TAG_LANGUAGE_NAME) if code[0]: if not name[0]: name = code #tracks.append((code[1], name[1])) tracks.append((n, name[1])) else: # some streams don't return track id tracks.append((n, "Track %d" % (n+1))) n += 1 return track, tracks def parse_url(self, url): if url.startswith("dvb-t://"): return dict([x.split('=', 1) for x in url[8:].split(":")]) parsed = urlparse.urlparse(url) return dict( scheme = parsed.scheme, path = parsed.path, hostname = parsed.hostname, port = parsed.port ) def show_pipeline(self, bin=None, pad=""): if bin is None: #self.relink_deinterlace() bin = self.player if hasattr(bin, "children"): for child in bin.children: f = child.get_factory() if not f: f = child print "%s %s [%s]" % (pad, f.get_name(), child.get_name()) self.show_pipeline(child, pad+" ") class GSTPlayer(BasePlayerWidget): """ GST player widget. """ name = 'gst' max_volume = 100 _language_priority = [] duration = 0 video_width = -1 video_height = -1 def __init__(self, media=None, subtitles=[], options={}, events=True): self.player = None # not initialized yet # minimal size self.last_pos = 0 self.handle_events = True if media and type(media)!=int: if media.startswith("dvd://"): self.handle_events = False if os.path.isdir(os.path.join(media, "VIDEO_TS")): self.handle_events = False # init Gst player self.gstp = GST_cmd( uri=media, subtitles=subtitles, options=options ) self.player = self.gstp.player # BUS self.bus = self.player.get_bus() self.bus.add_signal_watch() self.bus.enable_sync_message_emission() self.bus.connect("message::eos", self.on_message_eos) self.bus.connect("message::error", self.on_message_error) #self.bus.connect("message::buffering", self.on_message_buffering) #self.bus.connect("message::tag", self.on_message_tag) self.bus.connect("message", self.on_message) self.gstp.playbin.connect("audio-tags-changed", self.on_audio_tag) #self.bus.connect("message::tag", self.on_message_tag) self.bus.connect("sync-message::element", self.on_sync_message) if media: self.set_media(media, options) def destroy(self): self.stop() def set_media(self, url, options): self.stop() self.eos_received = False self.current_program = None self.last_stats = {} self.error_count = 0 self.duration = 0 self.msg_cache = {} self.tdt = None self.eit_event = {} self.gstp.set_uri(url, options) self.start_time = time.time() self.paused = False def on_sync_message(self, bus, msg): if not hasattr(self, 'xid'): return if msg.get_structure().get_name() == "prepare-window-handle": msg.src.set_property("force-aspect-ratio", True) Gdk.threads_enter() msg.src.set_window_handle(self.xid) Gdk.threads_leave() def on_message_tag(self, bus, msg): tag = msg.parse_tag() for capid in range(tag.n_tags()): cap = tag.nth_tag_name(capid) #print cap.keys() if cap=='width': self.video_width = tag.get_int(cap)[1] elif cap=='height': self.video_height = tag.get_int(cap)[1] elif cap=='pixel-aspect-ratio': self.video_aspect_ratio = tag.get_float(cap)[1] elif cap=='language-code': lang = tag.get_string(cap)[1] if lang not in [x[0] for x in self.audio_tracks]: self.audio_tracks.append([lang, lang]) elif cap in ['video-codec', 'minimum-bitrate','bitrate','maximum-bitrate' ]: pass else: print "MSG TAG CAP:", cap#, tag.to_string() def on_message_eos(self, bus, msg): print "Message: EOS" self.player.set_state(Gst.State.NULL) self.eos_received = True return True def on_message_error(self, bus, msg): print "ON ERROR:", msg #self.player.set_state(Gst.State.NULL) err, debug = msg.parse_error() print "Error: %s" % err, debug self.error_count += 1000 return True def on_message_buffering(self, bus, msg): percent = msg.parse_buffering() print "Buffer fill %d%%" % percent def on_message(self, bus, msg): msgs = msg.get_structure() if not msgs: return msgn = msgs.get_name() try: self.msg_cache[msgn] = getattr(msgs.get_value("section"), "get_"+msgn)() except AttributeError: pass if msgn=="pmt": #print "PMT:", msgs.to_string() pmt = msgs.get_value("section").get_pmt() if Debug("pmt"): print [x.parse_dvb_short_event() for x in pmt.descriptors] print [x.parse_dvb_network_name() for x in pmt.descriptors] print [x.parse_logical_channel() for x in pmt.descriptors] print [x.parse_iso_639_language() for x in pmt.descriptors] import IPython;IPython.embed() print 'pcr_pid', pmt.pcr_pid elif msgn=="eit": eit = msgs.get_value("section").get_eit() # search for current program #service_id = msgs.get_uint("service-id") #if not service_id[0] or service_id[1]!=self.current_program: # return if eit.actual_stream and eit.events: for event in eit.events: if event.running_status==GstMpegts.RunningStatus.RUNNING: #self.eit_event = dict( # name = event.descriptors[0].parse_dvb_short_event()[2], # start = gstdatetime(event.start_time), # duration = event.duration #) print 'eit_event descr=%s, eid=%s, mode=%s, last_tab_id=%s, trasport_stream_id=%s, segm_l_sec=%s, net_id=%s, present_following=%s' % ( event.descriptors[0].parse_dvb_short_event()[2], event.event_id, event.free_CA_mode, eit.last_table_id, eit.transport_stream_id, eit.segment_last_section_number, eit.original_network_id, eit.present_following ) #print event.descriptors[0].parse_dvb_short_event()[2],\ # event.descriptors[0].tag, event.descriptors[0].tag_extension,\ # ""#event.descriptors[0].parse_dvb_service() if Debug("eit"): import IPython;IPython.embed() elif msgn=="dvb-frontend-stats": #print msgn.upper(), msgs.to_string() self.last_stats = dict( status = msgs.get_int("status")[1], signal = msgs.get_int("signal")[1], snr = msgs.get_int("snr")[1], ber = msgs.get_int("ber")[1], unc = msgs.get_int("unc")[1], lock = msgs.get_boolean("lock")[1] ) elif msgn=="dvb-adapter": pass # ignore elif msgn.startswith("Gst"): pass # ignore internal messages elif msgn=="nit": nit = msgs.to_string() # network info #print "NIT", nit elif msgn=="pat": pat = msgs.to_string() #print "PAT", msgs.to_string() elif msgn=="sdt": sdt = msgs.to_string() #print "SDT", msgs.to_string() elif msgn in ["section", "missing-plugin", "prepare-window-handle"]: pass # ignore elif msgn=='tdt': dt = msgs.get_value("section").get_tdt() self.tdt = gstdatetime(dt) elif msgn=="tot": dt = msgs.get_value("section").get_tot().utc_time self.tdt = gstdatetime(dt) else: print msgs.to_string() print "MSG:", msgn, msgs.to_string() def on_audio_tag(self, playbin, stream): # set 1st language by priority tracks = dict([(x[1], x[0]) for x in self.audio_tracks]) current_lang = self.audio_track for lang in self._language_priority: if lang in tracks: if current_lang != tracks[lang]: print "Switching prioritized language: %s" % lang self.set_track(tracks[lang]) return def get_message(self): if self.eit_event.get('name'): return self.eit_event['name'] def play(self): self.player.set_state(Gst.State.PLAYING) self.player.get_state(nanosecond/50) # 0.1 s timeout def pause(self, state=None): if state is None: self.paused = not self.paused else: self.paused = state if self.paused: self.player.set_state(Gst.State.PAUSED) else: self.player.set_state(Gst.State.PLAYING) self.player.get_state(nanosecond/10) # 0.1 s timeout def stop(self): if self.player: self.player.set_state(Gst.State.NULL) self.player.get_state(nanosecond) # 1 s timeout def decode_errors(self): return int(self.last_stats.get('ber', 0))/1024 + self.error_count def read_bytes(self): read_bytes = self.player.query_position(Gst.Format.BYTES) if read_bytes[0]: return read_bytes[1] if self.last_stats.get('lock')==False: return 0 # return 1 if unable to query position return 1 def language_priority(self, priority): self._language_priority = priority def get_state(self, timeout=10): s = self.player.get_state(timeout)[1] if s == Gst.State.PAUSED: return "Paused" elif s == Gst.State.PLAYING: return "Playing" return s.value_nick def get_size(self): sample = self.gstp.playbin.get_property("sample") caps = sample.get_caps() try: for cap in caps: self.video_width = cap['width'] self.video_height = cap['height'] except TypeError: pass return (self.video_width, self.video_height) def was_ended(self): return self.eos_received def get_volume(self): return self.gstp.volume.get_property("volume")*100.0 def set_volume(self, value): self.gstp.volume.set_property("volume", 0.01*value) def get_mute(self): return bool(self.gstp.volume.get_property('mute')) def set_mute(self, value): self.gstp.volume.set_property("mute", value) def toggle_mute(self): self.gstp.volume.set_property("mute", not self.gstp.volume.get_property("mute")) def print_info(self): print '-'*78 print "Pipeline" self.gstp.show_pipeline() print '-'*78 print "Player: GStreamer" print "Size:", self.get_size() print "Pos (%):", \ self.player.query_position(Gst.Format.PERCENT)[1]/percent_scale print "Pos (s):", \ self.player.query_position(Gst.Format.TIME)[1]/nanosecond print "Duration (s):", self.duration/nanosecond print "Errors:", self.last_stats print '-'*78 def get_position(self): pos = self.gstp.video.query_position(Gst.Format.TIME) if pos[0]: ns = pos[1] else: ns = self.last_pos if self.duration<=0: dur = self.gstp.video.query_duration(Gst.Format.TIME) if dur[0] and dur[1]>0: self.duration = dur[1] if self.duration<=0: if self.eit_event and self.tdt: # return value from event, not from position pos = (self.tdt - self.eit_event['start']).seconds duration = self.eit_event['duration'] return 100.0*pos/duration, pos, duration return 0, 0, 0 if self.duration==0: return 0.0, ns/nanosecond, 0.0 return 100.0*ns/self.duration, ns/nanosecond, self.duration/nanosecond def forward(self, seconds=10): # wait for unfinished seek #state = self.player.get_state(nanosecond/100) #if state[2]!=Gst.State.VOID_PENDING: # # unable to process now, something is pending # print "You are too fast, operation still pending:", state # return # seek pos = self.gstp.video.query_position(Gst.Format.TIME) if not pos[0]: return t = pos[1] # do not seek forward on ended streams if t+nanosecond*seconds>=self.duration: print "GST: Seek over 100% skipped." return self.last_pos = max(t+nanosecond*seconds, 0) if self.duration!=0: print "GST: Seek to: %s, %s%%" % ( self.last_pos/nanosecond, float(t+nanosecond*seconds)/self.duration ) self.player.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, self.last_pos ) state = self.player.get_state(nanosecond/100) # clear EOS flag on seek self.eos_received = False def set_deinterlace(self, value): print "GST: deinterlace: %s" % value if self.gstp.deinterlacer: # force on(1) or off(2) self.gstp.deinterlacer.set_property("mode", value and 1 or 2) else: self.gstp.update_flags(value and 0x200 or -0x200) def get_crop(self): filter = self.gstp.playbin.get_property("video-filter") if not filter: return None try: return self.last_crop except AttributeError: return None def set_crop(self, crop): self.last_crop = crop self.stop() self.gstp.playbin.set_property("video-filter", None) self.play() time.sleep(0.1) width, height = self.get_size() if not crop: return elif crop=="auto": self.crop = (-1, -1, -1, -1) elif crop=="16:9" or crop=="16:10": new_height = width * int(crop.split(":")[-1]) / 16 if new_height>=height: print "CROP: %s same size calculated, ignoring" % crop return crop_size = (height - new_height)/2 self.crop = (crop_size, 0, crop_size, 0) else: print "CROP: undefined:", crop return print "CROP: %s, %d x %d, %s" % (crop, width, height, self.crop) crop_filter = Gst.parse_bin_from_description( "videocrop top=%d right=%d bottom=%d left=%d" % self.crop, True ) self.stop() self.gstp.playbin.set_property("video-filter", crop_filter) self.play() return @property def audio_track(self): current, all = self.gstp.get_tracks("audio") #print "Current track:", current, all try: return all[current][0] except IndexError: return current @property def audio_tracks(self): current, all = self.gstp.get_tracks("audio") return all def set_track(self, track): print "Setting audio track:", track audids = [x[0] for x in self.audio_tracks] if track in audids: self.gstp.set_track("audio", audids.index(track)) else: self.gstp.set_track("audio", -1) def sub_list(self): current, all = self.gstp.get_tracks("text") if all: all.append((len(all), "disabled")) print "SUBLIST:", current, all return all @property def subtitle(self): current, all = self.gstp.get_tracks("text") flag = self.gstp.get_flag(GST_PLAY_FLAG_TEXT) print \ "GETSUB: flag=%s, current=%s, all=%s, suburi=%s, current-suburi=%s" \ % (flag, current, all, self.gstp.playbin.get_property("suburi"), self.gstp.playbin.get_property("current-suburi")) if not flag: # subtitles are disabled return len(all) return current def next_sub(self, idx=None, increment=1): subids = [x[0] for x in self.sub_list()] if not subids: return False if idx is None: idx = self.subtitle+increment if idx>=len(subids): idx = subids[0] elif idx<0: idx = subids[-1] else: idx = subids[idx] if idx==subids[-1]: # last subtitle self.gstp.update_flags(-GST_PLAY_FLAG_TEXT) else: if not idx in subids: return False self.gstp.update_flags(GST_PLAY_FLAG_TEXT) self.gstp.set_track("text", subids.index(idx)) return True def snapshot(self, path=None, filename="snapshot.png"): sample = self.gstp.playbin.emit("convert-sample", Gst.Caps("image/png")) buffer = sample.get_buffer() data = buffer.extract_dup(0, buffer.get_size()) if path: # save to file filename = os.path.join(path, filename) open(filename, "wb").write(data) print "Saved %s (%d bytes)." % (filename, len(data)) return data class GSTWidget(Gtk.DrawingArea, GSTPlayer): def __init__(self, media=None, subtitles=[], options={}, events=True): Gtk.DrawingArea.__init__(self) GSTPlayer.__init__(self, media, subtitles, options, events) self.set_size_request(320, 200) if self.handle_events: print "Setting events", events self.set_events( Gtk.gdk.BUTTON_PRESS_MASK |Gtk.gdk.BUTTON_RELEASE_MASK |Gtk.gdk.SCROLL_MASK |Gtk.gdk.POINTER_MOTION_MASK ) self.modify_bg(Gtk.STATE_NORMAL, COLOR_BLACK) def get_state(self, timeout=10): return GSTPlayer.get_state(self, timeout) def show_all(self): super(GSTWidget, self).show_all() window = self.get_property('window') try: self.xid = window.get_xid() except AttributeError: pass # Wayland? def destroy(self): self.stop() Gtk.DrawingArea.destroy(self) time.sleep(0.1)