1 #!/usr/bin/env python3.4
3 # This file is part of ABYSS.
5 # ABYSS is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # ABYSS is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with ABYSS. If not, see <http://www.gnu.org/licenses/>.
18 # Copyright (c) 2016 David Testé
20 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23 # - Implement a method to switch to webcam feed if Elphel cam feed is lost
24 # --> Use ping by opening Telnet connexion every 2 seconds (if it fails, then switch to webcam)
25 # --> Has to be threaded
26 # - Add a checkbox to enable/disable options (storing/streaming - storing only - stream only - etc...)
27 # - Add a function to get the ip address of the camera automatically (see github.com/paulmilliken)
28 # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc)
29 # --> Taken care in FAI building
30 # - Generate a log file during runtime. (e.g. this will let you know if the network configuration
31 # and the pipeline construction went well (or not))
32 # - Add an input source choice for the user (camera on IP or webcam)
33 # - Add a time counter
34 # --> Has to be threaded
35 # - Add a 'CPU load' widget
36 # - Add the FSF logo (need to do some pixel art) as an application icon
37 # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make()
38 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
41 # INFO: run the following command in a terminal before launching libre-streamer to get a error log.
42 # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1
43 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
45 __author__
= 'David Testé'
48 __maintainer__
= 'David Testé'
49 __email__
= 'soonum@gnu.org'
50 __status__
= 'Prototype'
54 from time
import time
, localtime
, strftime
57 gi
.require_version('Gtk', '3.0')
58 from gi
.repository
import Gtk
59 from gi
.repository
import Gdk
60 gi
.require_version('Gst', '1.0')
61 from gi
.repository
import Gst
62 from gi
.repository
import GdkX11
63 from gi
.repository
import GstVideo
64 from gi
.repository
import GObject
68 # Based on 2016 FSF's ELPHEL camera configuration
72 CAM1_IP1
= 'CAM_1: ' + IP_1
73 CAM2_IP2
= 'CAM_1: ' + IP_2
74 CAM3_IP3
= 'CAM_1: ' + IP_3
75 ENTRYFIELD_TEXT
= 'Please fill both entry\nfield to stop streaming.'
77 formatted_date
= strftime('%Y_%m_%d', localtime())
78 metadata
= {'speaker_name':'NC',
84 class Streamgui(object):
90 self
.win
= Gtk
.Window()
91 self
.win
.set_title("ABYSS")
92 self
.win
.connect("delete_event",
93 lambda w
,e
: Gtk
.main_quit())
94 ## self.win.fullscreen()
95 vbox
= Gtk
.VBox(False, 0)
96 vbox_labels
= Gtk
.VBox(False, 0)
97 vbox_entries
= Gtk
.VBox(False, 0)
98 vbox_streaminfo
= Gtk
.VBox(False, 0)
99 vbox_cpuinfo
= Gtk
.VBox(False, 0)
100 vbox_rbuttongrp
= Gtk
.VBox(False, 0)
101 hbox
= Gtk
.HBox(False, 30)
102 hbox_videoaudio
= Gtk
.HBox(False, 0)
103 hbox_time
= Gtk
.HBox(False, 0)
105 self
.videowidget
= Gtk
.DrawingArea()
106 self
.videowidget
.set_size_request(600, 400)
109 # True stereo feed has to be implemented:
110 self
.vumeter_l
= Gtk
.ProgressBar()
111 self
.vumeter_l
.set_orientation(Gtk
.Orientation
.VERTICAL
)
112 self
.vumeter_l
.set_inverted(True)
113 self
.vumeter_r
= Gtk
.ProgressBar()
114 self
.vumeter_r
.set_orientation(Gtk
.Orientation
.VERTICAL
)
115 self
.vumeter_r
.set_inverted(True)
116 ## Use CSS to modify the color of ProgressBar
117 ## color = Gdk.RGBA()
118 ## Gdk.RGBA.parse(color, 'rgb(240,0,150)')
119 ## print ("Color: ", color)
120 ## self.vumeter.override_background_color(Gtk.StateFlags.NORMAL, color)
121 ## self.vumeter.override_symbolic_color('bg_color', color)
122 ## self.vumeter.override_symbolic_color('theme_bg_color', color)
124 self
.baseinfo_label
= Gtk
.Label('Base info: ')
125 self
.baseinfo_entry_label
= Gtk
.Label('LP_' + formatted_date
)
126 self
.speakerinfo_label
= Gtk
.Label('Speaker name: ')
127 self
.speakerinfo_entry
= Gtk
.Entry()
128 self
.sessioninfo_label
= Gtk
.Label('Session name: ')
129 self
.sessioninfo_entry
= Gtk
.Entry()
131 self
.stream_button
= Gtk
.Button("Stream")
132 self
.stream_button
.connect("clicked", self
.on_stream_clicked
)
133 self
.streamtime_label
= Gtk
.Label('Time elapsed ')
134 self
.streamtime_value
= Gtk
.Label('00:00:00')
136 self
.cpuload_label
= Gtk
.Label('CPU load: ')
137 self
.cpuload_value
= Gtk
.Label('NC')
139 self
.cam1_rbutton
= Gtk
.RadioButton(None, label
=CAM1_IP1
)
140 self
.cam2_rbutton
= Gtk
.RadioButton(self
.cam1_rbutton
, label
=CAM2_IP2
)
141 self
.cam3_rbutton
= Gtk
.RadioButton(self
.cam1_rbutton
, label
=CAM3_IP3
)
143 self
.entryfield_info
= Gtk
.MessageDialog(buttons
=Gtk
.ButtonsType
.CLOSE
,
144 text
=ENTRYFIELD_TEXT
,)
145 ##messagetype=Gtk.MessageType.WARNING,
146 ##Gtk.MessageType.INFO,)
148 hbox_videoaudio
.pack_start(self
.videowidget
, True, True, 0)
149 hbox_videoaudio
.pack_start(self
.vumeter_l
, False, False, 3)
150 hbox_videoaudio
.pack_start(self
.vumeter_r
, False, False, 3)
151 vbox_labels
.pack_start(self
.baseinfo_label
, True, True, 0)
152 vbox_labels
.pack_start(self
.speakerinfo_label
, True, True, 0)
153 vbox_labels
.pack_start(self
.sessioninfo_label
, True, True, 0)
154 vbox_entries
.pack_start(self
.baseinfo_entry_label
, True, True, 0)
155 vbox_entries
.pack_start(self
.speakerinfo_entry
, True, True, 0)
156 vbox_entries
.pack_start(self
.sessioninfo_entry
, True, True, 0)
157 vbox_streaminfo
.pack_start(self
.stream_button
, False, True, 15)
158 hbox_time
.pack_start(self
.streamtime_label
, False, False, 0)
159 hbox_time
.pack_start(self
.streamtime_value
, False, False, 0)
160 vbox_streaminfo
.pack_start(hbox_time
, False, True, 0)
161 vbox_rbuttongrp
.pack_start(self
.cam1_rbutton
, False, False, 0)
162 vbox_rbuttongrp
.pack_start(self
.cam2_rbutton
, False, False, 0)
163 vbox_rbuttongrp
.pack_start(self
.cam3_rbutton
, False, False, 0)
164 hbox
.pack_start(vbox_labels
, False, False, 0)
165 hbox
.pack_start(vbox_entries
, False, False, 0)
166 hbox
.pack_start(vbox_streaminfo
, False, False, 0)
167 hbox
.pack_start(self
.cpuload_label
, False, False, 0)
168 hbox
.pack_start(self
.cpuload_value
, False, False, 0)
169 hbox
.pack_start(vbox_rbuttongrp
, False, False, 0)
170 vbox
.pack_start(hbox_videoaudio
, True, True, 0)
171 vbox
.pack_start(hbox
, False, True, 0)
174 self
.win
.set_position(Gtk
.WindowPosition
.CENTER
)
177 self
.xid
= self
.videowidget
.get_property('window').get_xid()
179 self
.create_pipeline_instance()
181 def create_pipeline_instance(self
, feed
='main'):
182 """Creates pipeline instance and attaches it to GUI."""
183 self
.pipel
= gstconf
.New_user_pipeline(feed
)
184 bus
= gstconf
.get_gstreamer_bus()
185 bus
.connect('sync-message::element', self
.on_sync_message
)
186 bus
.connect('message', self
.on_message
)
187 # Try to use 'sync-message::element' instead of 'message'
189 def create_backup_pipeline(self
):
190 labelname
= self
.stream_button
.get_label()
191 if labelname
== 'ON AIR':
192 self
.create_pipeline_instance(feed
='backup')
193 self
.pipel
.stream_play()
195 def on_sync_message(self
, bus
, message
):
197 if message
.get_structure().get_name() == 'prepare-window-handle':
198 imagesink
= message
.src
199 imagesink
.set_property('force-aspect-ratio', True)
200 imagesink
.set_window_handle(self
.videowidget
.get_property('window').get_xid())
202 def on_message(self
, bus
, message
):
203 # Getting the RMS audio level value:
204 s
= Gst
.Message
.get_structure(message
)
205 if message
.type == Gst
.MessageType
.ELEMENT
:
206 if str(Gst
.Structure
.get_name(s
)) == 'level':
207 pct
= self
.iec_scale(s
.get_value('rms')[0])
208 ##print('Level value: ', pct, '%') # [DEBUG]
209 self
.vumeter_l
.set_fraction(pct
)
210 self
.vumeter_r
.set_fraction(pct
)
211 # Watching for feed loss during streaming:
213 if t
== Gst
.MessageType
.ERROR
:
214 err
, debug
= message
.parse_error()
215 if '(651)' not in debug
:
216 # The error is not a socket error.
217 self
.pipel
.stream_stop()
218 self
.build_filename(streamfailed
=True)
219 self
.create_backup_pipeline()
221 def on_stream_clicked(self
, widget
):
222 labelname
= self
.stream_button
.get_label()
223 if labelname
== 'Stream':
224 if self
.pipel
.feed
== 'backup':
225 # Get back to main feed:
226 self
.create_pipeline_instance()
227 self
.clean_entry_fields()
228 self
.pipel
.stream_play()
229 self
.stream_button
.set_label('ON AIR')
231 elif labelname
== 'ON AIR':
232 if self
.build_filename():
233 self
.pipel
.stream_stop()
234 self
.stream_button
.set_label('Stream')
236 def build_filename(self
, streamfailed
=False):
237 """Get text in entries, check if empty and apply formatting if needed."""
239 base
= self
.baseinfo_entry_label
.get_text()
240 speaker
= self
.speakerinfo_entry
.get_text()
241 speaker
= sep
.join(speaker
.split())
242 session
= self
.sessioninfo_entry
.get_text()
243 session
= sep
.join(session
.split())
244 raw_filename
= base
+ sep
+ speaker
+ sep
+ session
246 if speaker
and session
:
247 if len(raw_filename
) >= maxlen
:
248 offset
= len(raw_filename
) - maxlen
249 raw_filename
= raw_filename
[:-offset
]
251 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
253 self
.pipel
.set_filenames(raw_filename
,)
254 print('RAWFILENAM: ', raw_filename
, ' <--')
256 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
258 elif not streamfailed
:
259 self
.entryfield_info
.run()
260 self
.entryfield_info
.hide()
264 def clean_entry_fields(self
):
265 self
.speakerinfo_entry
.set_text('')
266 self
.sessioninfo_entry
.set_text('')
268 def iec_scale(self
, db
):
269 """Returns the meter deflection percentage given a db value."""
275 pct
= (db
+ 70.0) * 0.25
277 pct
= (db
+ 60.0) * 0.5 + 2.5
279 pct
= (db
+ 50.0) * 0.75 + 7.5
281 pct
= (db
+ 40.0) * 1.5 + 15.0
283 pct
= (db
+ 30.0) * 2.0 + 30.0
285 pct
= (db
+ 20.0) * 2.5 + 50.0
290 ## Use threading module to refresh the time elapsed sinc the begining of the stream??
291 def time_elapsed(self
, widget
):
292 if self
.pipel
.stream_get_state() == 'PLAYING':
296 if __name__
== "__main__":