#!/usr/bin/env python3.4 # -*- coding: utf-8 -*- # This file is part of ABYSS. # ABYSS Broadcast Your Streaming Successfully # # ABYSS 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. # # ABYSS 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 ABYSS. If not, see . # # Copyright (c) 2016 David Testé # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TODO list: # ---------- # - Implement a method to switch to webcam feed if Elphel cam feed is lost # --> Use ping by opening Telnet connexion every 2 seconds (if it fails, then switch to webcam) # --> Has to be threaded # - 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) # --> Taken care in FAI building # - 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 # --> Has to be threaded # - 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 # Based on 2016 FSF's ELPHEL camera configuration ##PORT = ':554' ##IP_1 = '192.168.48.2' ##IP_2 = '192.168.48.3' ##IP_3 = '192.168.48.4' ##CAM1_IP1 = 'CAM_1: ' + IP_1 ##CAM2_IP2 = 'CAM_2: ' + IP_2 ##CAM3_IP3 = 'CAM_3: ' + IP_3 ##rtsp_address = None ENTRYFIELD_TEXT = 'Please fill both entry field to stop streaming.' CAMCHOICE_TEXT = 'Please choose a camera address.' TESTMODE_TEXT = 'Quit testing mode to switch to streaming mode.' 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): # Initialize a pipeline self.pipel = None # Create the GUI self.win = Gtk.Window() self.win.set_title("ABYSS") 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(True, 0) vbox_tbuttongrp = Gtk.VBox(False, 0) hbox = Gtk.HBox(False, 30) hbox_videoaudio = Gtk.HBox(False, 0) hbox_time = Gtk.HBox(False, 0) hbox_cpu = Gtk.HBox(False, 0) self.videowidget = Gtk.DrawingArea() self.videowidget.set_size_request(800, 600) # 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.test_button = Gtk.Button('Set-up test') self.test_button.connect('clicked', self.on_test_clicked) self.cpuload_label = Gtk.Label('CPU load: ') self.cpuload_value = Gtk.Label('NC') ## self.cam1_tbutton = Gtk.ToggleButton(None, label=CAM1_IP1) ## self.cam1_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam1') ## self.cam2_tbutton = Gtk.ToggleButton(self.cam1_tbutton) ## self.cam2_tbutton.set_label(CAM2_IP2) ## self.cam2_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam2') ## self.cam3_tbutton = Gtk.ToggleButton(self.cam1_tbutton) ## self.cam3_tbutton.set_label(CAM3_IP3) ## self.cam3_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam3') self.entryfield_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, text=ENTRYFIELD_TEXT,) ##messagetype=Gtk.MessageType.WARNING, ##Gtk.MessageType.INFO,) ## self.camchoice_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, ## text=CAMCHOICE_TEXT,) self.testmode_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, text=TESTMODE_TEXT,) 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) hbox_time.pack_start(self.streamtime_label, False, False, 0) hbox_time.pack_start(self.streamtime_value, False, False, 0) hbox_cpu.pack_start(self.cpuload_label, False, False, 0) hbox_cpu.pack_start(self.cpuload_value, False, False, 0) vbox_streaminfo.pack_start(hbox_time, False, True, 0) vbox_streaminfo.pack_start(hbox_cpu, False, True, 0) ## vbox_tbuttongrp.pack_start(self.cam1_tbutton, False, False, 0) ## vbox_tbuttongrp.pack_start(self.cam2_tbutton, False, False, 0) ## vbox_tbuttongrp.pack_start(self.cam3_tbutton, False, False, 0) hbox.pack_start(vbox_labels, False, False, 0) hbox.pack_start(vbox_entries, False, False, 0) ## hbox.pack_start(vbox_tbuttongrp, False, False, 0) hbox.pack_start(self.test_button, False, False, 0) hbox.pack_start(self.stream_button, False , False, 0) hbox.pack_start(vbox_streaminfo, 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() 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) return True 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.build_filename(streamfailed=True) self.create_backup_pipeline() def on_stream_clicked(self, widget): labelname1 = self.stream_button.get_label() labelname2 = self.test_button.get_label() if labelname1 == 'Stream': if labelname2 != 'Testing ...': if self.create_pipeline_instance(): self.clean_entry_fields() self.pipel.stream_play() self.stream_button.set_label('ON AIR') start_time = time() else: self.testmode_info.run() self.testmode_info.hide() elif labelname1 == 'ON AIR': if self.build_filename(): self.pipel.stream_stop() self.stream_button.set_label('Stream') def on_test_clicked(self, widget): labelname = self.test_button.get_label() if labelname == 'Set-up test': if self.create_pipeline_instance(feed='test'): self.pipel.stream_play() self.test_button.set_label('Testing ...') elif labelname == 'Testing ...': self.pipel.stream_stop() self.test_button.set_label('Set-up test') ## def on_tbutton_toggled(self, tbutton, name): ## global rtsp_address ## running_cond = (self.stream_button.get_label() == 'ON AIR' or ## self.test_button.get_label() == 'Testing ...') ## if running_cond: ## tbutton.set_active(False) ## return ## ## if tbutton.get_active(): ## if name == 'cam1': ## self.cam2_tbutton.set_active(False) ## self.cam3_tbutton.set_active(False) ## rtsp_address = IP_1 + PORT ## elif name == 'cam2': ## self.cam1_tbutton.set_active(False) ## self.cam3_tbutton.set_active(False) ## rtsp_address = IP_2 + PORT ## elif name == 'cam3': ## self.cam1_tbutton.set_active(False) ## self.cam2_tbutton.set_active(False) ## rtsp_address = IP_3 + PORT def build_filename(self, streamfailed=False): """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] if streamfailed: self.pipel.set_filenames(raw_filename, streamfailed=True) else: self.pipel.set_filenames(raw_filename,) ## print('RAWFILENAME: ', raw_filename, ' <--') # [DEBUG] elif streamfailed: self.pipel.set_filenames(raw_filename, streamfailed=True) else: self.pipel.set_filenames(raw_filename,) return True elif not streamfailed: 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()