1 #!/usr/bin/env python3.4
2 # -*- coding: utf-8 -*-
4 # This file is part of ABYSS.
5 # ABYSS Broadcast Your Streaming Successfully
7 # ABYSS is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # ABYSS is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with ABYSS. If not, see <http://www.gnu.org/licenses/>.
20 # Copyright (c) 2016 David Testé
22 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25 # - Implement a method to switch to webcam feed if Elphel cam feed is lost
26 # --> Use ping by opening Telnet connexion every 2 seconds (if it fails, then switch to webcam)
27 # --> Has to be threaded
28 # - Add a checkbox to enable/disable options (storing/streaming - storing only - stream only - etc...)
29 # - Add a function to get the ip address of the camera automatically (see github.com/paulmilliken)
30 # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc)
31 # --> Taken care in FAI building
32 # - Generate a log file during runtime. (e.g. this will let you know if the network configuration
33 # and the pipeline construction went well (or not))
34 # - Add an input source choice for the user (camera on IP or webcam)
35 # - Add a time counter
36 # --> Has to be threaded
37 # - Add a 'CPU load' widget
38 # - Add the FSF logo (need to do some pixel art) as an application icon
39 # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make()
40 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
42 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
43 # INFO: run the following command in a terminal before launching libre-streamer to get a error log.
44 # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1
45 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
47 __author__
= 'David Testé'
50 __maintainer__
= 'David Testé'
51 __email__
= 'soonum@gnu.org'
52 __status__
= 'Prototype'
56 from time
import time
, localtime
, strftime
59 gi
.require_version('Gtk', '3.0')
60 from gi
.repository
import Gtk
61 from gi
.repository
import Gdk
62 gi
.require_version('Gst', '1.0')
63 from gi
.repository
import Gst
64 from gi
.repository
import GdkX11
65 from gi
.repository
import GstVideo
66 from gi
.repository
import GObject
70 # Based on 2016 FSF's ELPHEL camera configuration
72 ##IP_1 = '192.168.48.2'
73 ##IP_2 = '192.168.48.3'
74 ##IP_3 = '192.168.48.4'
75 ##CAM1_IP1 = 'CAM_1: ' + IP_1
76 ##CAM2_IP2 = 'CAM_2: ' + IP_2
77 ##CAM3_IP3 = 'CAM_3: ' + IP_3
79 ENTRYFIELD_TEXT
= 'Please fill both entry field to stop streaming.'
80 CAMCHOICE_TEXT
= 'Please choose a camera address.'
81 TESTMODE_TEXT
= 'Quit testing mode to switch to streaming mode.'
82 formatted_date
= strftime('%Y_%m_%d', localtime())
83 metadata
= {'speaker_name':'NC',
89 class Streamgui(object):
94 # Initialize a pipeline
98 self
.win
= Gtk
.Window()
99 self
.win
.set_title("ABYSS")
100 self
.win
.connect("delete_event",
101 lambda w
,e
: Gtk
.main_quit())
102 ## self.win.fullscreen()
103 vbox
= Gtk
.VBox(False, 0)
104 vbox_labels
= Gtk
.VBox(False, 0)
105 vbox_entries
= Gtk
.VBox(False, 0)
106 vbox_streaminfo
= Gtk
.VBox(True, 0)
107 vbox_tbuttongrp
= Gtk
.VBox(False, 0)
108 hbox
= Gtk
.HBox(False, 30)
109 hbox_videoaudio
= Gtk
.HBox(False, 0)
110 hbox_time
= Gtk
.HBox(False, 0)
111 hbox_cpu
= Gtk
.HBox(False, 0)
113 self
.videowidget
= Gtk
.DrawingArea()
114 self
.videowidget
.set_size_request(800, 600)
117 # True stereo feed has to be implemented:
118 self
.vumeter_l
= Gtk
.ProgressBar()
119 self
.vumeter_l
.set_orientation(Gtk
.Orientation
.VERTICAL
)
120 self
.vumeter_l
.set_inverted(True)
121 self
.vumeter_r
= Gtk
.ProgressBar()
122 self
.vumeter_r
.set_orientation(Gtk
.Orientation
.VERTICAL
)
123 self
.vumeter_r
.set_inverted(True)
124 ## Use CSS to modify the color of ProgressBar
125 ## color = Gdk.RGBA()
126 ## Gdk.RGBA.parse(color, 'rgb(240,0,150)')
127 ## print ("Color: ", color)
128 ## self.vumeter.override_background_color(Gtk.StateFlags.NORMAL, color)
129 ## self.vumeter.override_symbolic_color('bg_color', color)
130 ## self.vumeter.override_symbolic_color('theme_bg_color', color)
132 self
.baseinfo_label
= Gtk
.Label('Base info: ')
133 self
.baseinfo_entry_label
= Gtk
.Label('LP_' + formatted_date
)
134 self
.speakerinfo_label
= Gtk
.Label('Speaker name: ')
135 self
.speakerinfo_entry
= Gtk
.Entry()
136 self
.sessioninfo_label
= Gtk
.Label('Session name: ')
137 self
.sessioninfo_entry
= Gtk
.Entry()
139 self
.stream_button
= Gtk
.Button('Stream')
140 self
.stream_button
.connect('clicked', self
.on_stream_clicked
)
141 self
.streamtime_label
= Gtk
.Label('Time elapsed ')
142 self
.streamtime_value
= Gtk
.Label('00:00:00')
143 self
.test_button
= Gtk
.Button('Set-up test')
144 self
.test_button
.connect('clicked', self
.on_test_clicked
)
146 self
.cpuload_label
= Gtk
.Label('CPU load: ')
147 self
.cpuload_value
= Gtk
.Label('NC')
149 ## self.cam1_tbutton = Gtk.ToggleButton(None, label=CAM1_IP1)
150 ## self.cam1_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam1')
151 ## self.cam2_tbutton = Gtk.ToggleButton(self.cam1_tbutton)
152 ## self.cam2_tbutton.set_label(CAM2_IP2)
153 ## self.cam2_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam2')
154 ## self.cam3_tbutton = Gtk.ToggleButton(self.cam1_tbutton)
155 ## self.cam3_tbutton.set_label(CAM3_IP3)
156 ## self.cam3_tbutton.connect('toggled', self.on_tbutton_toggled, 'cam3')
158 self
.entryfield_info
= Gtk
.MessageDialog(buttons
=Gtk
.ButtonsType
.CLOSE
,
159 text
=ENTRYFIELD_TEXT
,)
160 ##messagetype=Gtk.MessageType.WARNING,
161 ##Gtk.MessageType.INFO,)
162 ## self.camchoice_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE,
163 ## text=CAMCHOICE_TEXT,)
164 self
.testmode_info
= Gtk
.MessageDialog(buttons
=Gtk
.ButtonsType
.CLOSE
,
167 hbox_videoaudio
.pack_start(self
.videowidget
, True, True, 0)
168 hbox_videoaudio
.pack_start(self
.vumeter_l
, False, False, 3)
169 hbox_videoaudio
.pack_start(self
.vumeter_r
, False, False, 3)
170 vbox_labels
.pack_start(self
.baseinfo_label
, True, True, 0)
171 vbox_labels
.pack_start(self
.speakerinfo_label
, True, True, 0)
172 vbox_labels
.pack_start(self
.sessioninfo_label
, True, True, 0)
173 vbox_entries
.pack_start(self
.baseinfo_entry_label
, True, True, 0)
174 vbox_entries
.pack_start(self
.speakerinfo_entry
, True, True, 0)
175 vbox_entries
.pack_start(self
.sessioninfo_entry
, True, True, 0)
176 hbox_time
.pack_start(self
.streamtime_label
, False, False, 0)
177 hbox_time
.pack_start(self
.streamtime_value
, False, False, 0)
178 hbox_cpu
.pack_start(self
.cpuload_label
, False, False, 0)
179 hbox_cpu
.pack_start(self
.cpuload_value
, False, False, 0)
180 vbox_streaminfo
.pack_start(hbox_time
, False, True, 0)
181 vbox_streaminfo
.pack_start(hbox_cpu
, False, True, 0)
182 ## vbox_tbuttongrp.pack_start(self.cam1_tbutton, False, False, 0)
183 ## vbox_tbuttongrp.pack_start(self.cam2_tbutton, False, False, 0)
184 ## vbox_tbuttongrp.pack_start(self.cam3_tbutton, False, False, 0)
185 hbox
.pack_start(vbox_labels
, False, False, 0)
186 hbox
.pack_start(vbox_entries
, False, False, 0)
187 ## hbox.pack_start(vbox_tbuttongrp, False, False, 0)
188 hbox
.pack_start(self
.test_button
, False, False, 0)
189 hbox
.pack_start(self
.stream_button
, False , False, 0)
190 hbox
.pack_start(vbox_streaminfo
, False, False, 0)
191 vbox
.pack_start(hbox_videoaudio
, True, True, 0)
192 vbox
.pack_start(hbox
, False, True, 0)
195 self
.win
.set_position(Gtk
.WindowPosition
.CENTER
)
198 self
.xid
= self
.videowidget
.get_property('window').get_xid()
200 def create_pipeline_instance(self
, feed
='main'):
201 """Creates pipeline instance and attaches it to GUI."""
202 self
.pipel
= gstconf
.New_user_pipeline(feed
,)
203 bus
= gstconf
.get_gstreamer_bus()
204 bus
.connect('sync-message::element', self
.on_sync_message
)
205 bus
.connect('message', self
.on_message
)
208 def create_backup_pipeline(self
):
209 labelname
= self
.stream_button
.get_label()
210 if labelname
== 'ON AIR':
211 self
.create_pipeline_instance(feed
='backup')
212 self
.pipel
.stream_play()
214 def on_sync_message(self
, bus
, message
):
216 if message
.get_structure().get_name() == 'prepare-window-handle':
217 imagesink
= message
.src
218 imagesink
.set_property('force-aspect-ratio', True)
219 imagesink
.set_window_handle(self
.videowidget
.get_property('window').get_xid())
221 def on_message(self
, bus
, message
):
222 # Getting the RMS audio level value:
223 s
= Gst
.Message
.get_structure(message
)
224 if message
.type == Gst
.MessageType
.ELEMENT
:
225 if str(Gst
.Structure
.get_name(s
)) == 'level':
226 pct
= self
.iec_scale(s
.get_value('rms')[0])
227 ##print('Level value: ', pct, '%') # [DEBUG]
228 self
.vumeter_l
.set_fraction(pct
)
229 self
.vumeter_r
.set_fraction(pct
)
230 # Watching for feed loss during streaming:
232 if t
== Gst
.MessageType
.ERROR
:
233 err
, debug
= message
.parse_error()
234 if '(651)' not in debug
:
235 # The error is not a socket error.
236 self
.pipel
.stream_stop()
237 self
.build_filename(streamfailed
=True)
238 self
.create_backup_pipeline()
240 def on_stream_clicked(self
, widget
):
241 labelname1
= self
.stream_button
.get_label()
242 labelname2
= self
.test_button
.get_label()
243 if labelname1
== 'Stream':
244 if labelname2
!= 'Testing ...':
245 if self
.create_pipeline_instance():
246 self
.clean_entry_fields()
247 self
.pipel
.stream_play()
248 self
.stream_button
.set_label('ON AIR')
251 self
.testmode_info
.run()
252 self
.testmode_info
.hide()
253 elif labelname1
== 'ON AIR':
254 if self
.build_filename():
255 self
.pipel
.stream_stop()
256 self
.stream_button
.set_label('Stream')
258 def on_test_clicked(self
, widget
):
259 labelname
= self
.test_button
.get_label()
260 if labelname
== 'Set-up test':
261 if self
.create_pipeline_instance(feed
='test'):
262 self
.pipel
.stream_play()
263 self
.test_button
.set_label('Testing ...')
264 elif labelname
== 'Testing ...':
265 self
.pipel
.stream_stop()
266 self
.test_button
.set_label('Set-up test')
268 ## def on_tbutton_toggled(self, tbutton, name):
269 ## global rtsp_address
270 ## running_cond = (self.stream_button.get_label() == 'ON AIR' or
271 ## self.test_button.get_label() == 'Testing ...')
273 ## tbutton.set_active(False)
276 ## if tbutton.get_active():
277 ## if name == 'cam1':
278 ## self.cam2_tbutton.set_active(False)
279 ## self.cam3_tbutton.set_active(False)
280 ## rtsp_address = IP_1 + PORT
281 ## elif name == 'cam2':
282 ## self.cam1_tbutton.set_active(False)
283 ## self.cam3_tbutton.set_active(False)
284 ## rtsp_address = IP_2 + PORT
285 ## elif name == 'cam3':
286 ## self.cam1_tbutton.set_active(False)
287 ## self.cam2_tbutton.set_active(False)
288 ## rtsp_address = IP_3 + PORT
290 def build_filename(self
, streamfailed
=False):
291 """Get text in entries, check if empty and apply formatting if needed."""
293 base
= self
.baseinfo_entry_label
.get_text()
294 speaker
= self
.speakerinfo_entry
.get_text()
295 speaker
= sep
.join(speaker
.split())
296 session
= self
.sessioninfo_entry
.get_text()
297 session
= sep
.join(session
.split())
298 raw_filename
= base
+ sep
+ speaker
+ sep
+ session
300 if speaker
and session
:
301 if len(raw_filename
) >= maxlen
:
302 offset
= len(raw_filename
) - maxlen
303 raw_filename
= raw_filename
[:-offset
]
305 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
307 self
.pipel
.set_filenames(raw_filename
,)
308 ## print('RAWFILENAME: ', raw_filename, ' <--') # [DEBUG]
310 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
312 self
.pipel
.set_filenames(raw_filename
,)
314 elif not streamfailed
:
315 self
.entryfield_info
.run()
316 self
.entryfield_info
.hide()
320 def clean_entry_fields(self
):
321 self
.speakerinfo_entry
.set_text('')
322 self
.sessioninfo_entry
.set_text('')
324 def iec_scale(self
, db
):
325 """Returns the meter deflection percentage given a db value."""
331 pct
= (db
+ 70.0) * 0.25
333 pct
= (db
+ 60.0) * 0.5 + 2.5
335 pct
= (db
+ 50.0) * 0.75 + 7.5
337 pct
= (db
+ 40.0) * 1.5 + 15.0
339 pct
= (db
+ 30.0) * 2.0 + 30.0
341 pct
= (db
+ 20.0) * 2.5 + 50.0
346 ## Use threading module to refresh the time elapsed sinc the begining of the stream??
347 def time_elapsed(self
, widget
):
348 if self
.pipel
.stream_get_state() == 'PLAYING':
352 if __name__
== "__main__":