#!/usr/bin/env python3.4 # This file is part of Libre-Streamer. # # Libre-Streamer 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 3 of the License, or # (at your option) any later version. # # Libre-Streamer is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Libre-Streamer. If not, see . # # Copyright (c) 2016 David Testé # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TODO list: # ---------- # - Implement a method to switch to webcam feed if Elphel cam feed is lost # - Display the Gst element 'videotestsrc', in case of failure of the whole pipeline # - Add a checkbox to enable/disable options (storing/streaming - storing only - stream only - etc...) # - Add a function to get the ip address of the camera automatically (see github.com/paulmilliken) # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc) # - Generate a log file during runtime. (e.g. this will let you know if the network configuration # and the pipeline construction went well (or not)) # - Add an input source choice for the user (camera on IP or webcam) # - Add a time counter # - Add a 'CPU load' widget # - Add the FSF logo (need to do some pixel art) as an application icon # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make() # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # INFO: run the following command in a terminal before launching libre-streamer to get a error log. # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! __author__ = 'David Testé' __licence__ = 'GPLv3' __version__ = 0.1 __maintainer__ = 'David Testé' __email__ = 'soonum@gnu.org' __status__ = 'Prototype' import sys from time import time, localtime, strftime import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from gi.repository import Gdk gi.require_version('Gst', '1.0') from gi.repository import Gst from gi.repository import GdkX11 from gi.repository import GstVideo from gi.repository import GObject import gstconf ENTRYFIELD_TEXT = 'Please fill both entry\nfield to stop streaming.' formatted_date = strftime('%Y_%m_%d', localtime()) metadata = {'speaker_name':'NC', 'session_title':'NC', 'organisation':'NC',} start_time = 0 class Streamgui(object): def __init__(self): # Create the GUI self.win = Gtk.Window() self.win.set_title("Libre-Streamer") self.win.connect("delete_event", lambda w,e: Gtk.main_quit()) ## self.win.fullscreen() vbox = Gtk.VBox(False, 0) vbox_labels = Gtk.VBox(False, 0) vbox_entries = Gtk.VBox(False, 0) vbox_streaminfo = Gtk.VBox(False, 0) vbox_cpuinfo = Gtk.VBox(False, 0) hbox = Gtk.HBox(False, 30) hbox_videoaudio = Gtk.HBox(False, 0) hbox_time = Gtk.HBox(False, 0) self.videowidget = Gtk.DrawingArea() self.videowidget.set_size_request(600, 400) # True stereo feed has to be implemented: self.vumeter_l = Gtk.ProgressBar() self.vumeter_l.set_orientation(Gtk.Orientation.VERTICAL) self.vumeter_l.set_inverted(True) self.vumeter_r = Gtk.ProgressBar() self.vumeter_r.set_orientation(Gtk.Orientation.VERTICAL) self.vumeter_r.set_inverted(True) ## Use CSS to modify the color of ProgressBar ## color = Gdk.RGBA() ## Gdk.RGBA.parse(color, 'rgb(240,0,150)') ## print ("Color: ", color) ## self.vumeter.override_background_color(Gtk.StateFlags.NORMAL, color) ## self.vumeter.override_symbolic_color('bg_color', color) ## self.vumeter.override_symbolic_color('theme_bg_color', color) self.baseinfo_label = Gtk.Label('Base info: ') self.baseinfo_entry_label = Gtk.Label('LP_' + formatted_date) self.speakerinfo_label = Gtk.Label('Speaker name: ') self.speakerinfo_entry = Gtk.Entry() self.sessioninfo_label = Gtk.Label('Session name: ') self.sessioninfo_entry = Gtk.Entry() self.stream_button = Gtk.Button("Stream") self.stream_button.connect("clicked", self.on_stream_clicked) self.streamtime_label = Gtk.Label('Time elapsed ') self.streamtime_value = Gtk.Label('00:00:00') self.cpuload_label = Gtk.Label('CPU load: ') self.cpuload_value = Gtk.Label('NC') self.entryfield_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, text=ENTRYFIELD_TEXT,) ##messagetype=Gtk.MessageType.WARNING, ##Gtk.MessageType.INFO,) hbox_videoaudio.pack_start(self.videowidget, True, True, 0) hbox_videoaudio.pack_start(self.vumeter_l, False, False, 3) hbox_videoaudio.pack_start(self.vumeter_r, False, False, 3) vbox_labels.pack_start(self.baseinfo_label, True, True, 0) vbox_labels.pack_start(self.speakerinfo_label, True, True, 0) vbox_labels.pack_start(self.sessioninfo_label, True, True, 0) vbox_entries.pack_start(self.baseinfo_entry_label, True, True, 0) vbox_entries.pack_start(self.speakerinfo_entry, True, True, 0) vbox_entries.pack_start(self.sessioninfo_entry, True, True, 0) vbox_streaminfo.pack_start(self.stream_button, False, True, 15) hbox_time.pack_start(self.streamtime_label, False, False, 0) hbox_time.pack_start(self.streamtime_value, False, False, 0) vbox_streaminfo.pack_start(hbox_time, False, True, 0) hbox.pack_start(vbox_labels, False, False, 0) hbox.pack_start(vbox_entries, False, False, 0) hbox.pack_start(vbox_streaminfo, False, False, 0) hbox.pack_start(self.cpuload_label, False, False, 0) hbox.pack_start(self.cpuload_value, False, False, 0) vbox.pack_start(hbox_videoaudio, True, True, 0) vbox.pack_start(hbox, False, True, 0) self.win.add(vbox) self.win.set_position(Gtk.WindowPosition.CENTER) self.win.show_all() self.xid = self.videowidget.get_property('window').get_xid() self.create_pipeline_instance() def create_pipeline_instance(self, feed='main'): """Creates pipeline instance and attaches it to GUI.""" self.pipel = gstconf.New_user_pipeline(feed) bus = gstconf.get_gstreamer_bus() bus.connect('sync-message::element', self.on_sync_message) bus.connect('message', self.on_message) # Try to use 'sync-message::element' instead of 'message' def create_backup_pipeline(self): labelname = self.stream_button.get_label() if labelname == 'ON AIR': self.create_pipeline_instance(feed='backup') self.pipel.stream_play() def on_sync_message(self, bus, message): if message.get_structure().get_name() == 'prepare-window-handle': imagesink = message.src imagesink.set_property('force-aspect-ratio', True) imagesink.set_window_handle(self.videowidget.get_property('window').get_xid()) def on_message(self, bus, message): # Getting the RMS audio level value: s = Gst.Message.get_structure(message) if message.type == Gst.MessageType.ELEMENT: if str(Gst.Structure.get_name(s)) == 'level': pct = self.iec_scale(s.get_value('rms')[0]) ##print('Level value: ', pct, '%') # [DEBUG] self.vumeter_l.set_fraction(pct) self.vumeter_r.set_fraction(pct) # Watching for feed loss during streaming: t = message.type if t == Gst.MessageType.ERROR: err, debug = message.parse_error() if '(651)' not in debug: # The error is not a socket error. self.pipel.stream_stop() self.create_backup_pipeline() def on_stream_clicked(self, widget): labelname = self.stream_button.get_label() if labelname == 'Stream': if self.pipel.feed == 'backup': # Get back to main feed: self.create_pipeline_instance() self.clean_entry_fields() self.pipel.stream_play() self.stream_button.set_label('ON AIR') start_time = time() elif labelname == 'ON AIR': if self.build_filename(): self.pipel.stream_stop() self.stream_button.set_label('Stream') def build_filename(self): """Get text in entries, check if empty and apply formatting if needed.""" sep = '_' base = self.baseinfo_entry_label.get_text() speaker = self.speakerinfo_entry.get_text() speaker = sep.join(speaker.split()) session = self.sessioninfo_entry.get_text() session = sep.join(session.split()) raw_filename = base + sep + speaker + sep + session maxlen = 70 if speaker and session: if len(raw_filename) >= maxlen: offset = len(raw_filename) - maxlen raw_filename = raw_filename[:-offset] self.pipel.set_filenames(raw_filename) return True else: self.entryfield_info.run() self.entryfield_info.hide() return False def clean_entry_fields(self): self.speakerinfo_entry.set_text('') self.sessioninfo_entry.set_text('') def iec_scale(self, db): """Returns the meter deflection percentage given a db value.""" pct = 0.0 if db < -70.0: pct = 0.0 elif db < -60.0: pct = (db + 70.0) * 0.25 elif db < -50.0: pct = (db + 60.0) * 0.5 + 2.5 elif db < -40.0: pct = (db + 50.0) * 0.75 + 7.5 elif db < -30.0: pct = (db + 40.0) * 1.5 + 15.0 elif db < -20.0: pct = (db + 30.0) * 2.0 + 30.0 elif db < 0.0: pct = (db + 20.0) * 2.5 + 50.0 else: pct = 100.0 return pct / 100 ## Use threading module to refresh the time elapsed sinc the begining of the stream?? def time_elapsed(self, widget): if self.pipel.stream_get_state() == 'PLAYING': pass if __name__ == "__main__": Gst.init() Streamgui() Gtk.main()