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
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."""
203 self
.pipel
= gstconf
.New_user_pipeline(rtsp_address
, feed
,)
204 bus
= gstconf
.get_gstreamer_bus()
205 bus
.connect('sync-message::element', self
.on_sync_message
)
206 bus
.connect('message', self
.on_message
)
209 self
.camchoice_info
.run()
210 self
.camchoice_info
.hide()
213 def create_backup_pipeline(self
):
214 labelname
= self
.stream_button
.get_label()
215 if labelname
== 'ON AIR':
216 self
.create_pipeline_instance(feed
='backup')
217 self
.pipel
.stream_play()
219 def on_sync_message(self
, bus
, message
):
221 if message
.get_structure().get_name() == 'prepare-window-handle':
222 imagesink
= message
.src
223 imagesink
.set_property('force-aspect-ratio', True)
224 imagesink
.set_window_handle(self
.videowidget
.get_property('window').get_xid())
226 def on_message(self
, bus
, message
):
227 # Getting the RMS audio level value:
228 s
= Gst
.Message
.get_structure(message
)
229 if message
.type == Gst
.MessageType
.ELEMENT
:
230 if str(Gst
.Structure
.get_name(s
)) == 'level':
231 pct
= self
.iec_scale(s
.get_value('rms')[0])
232 ##print('Level value: ', pct, '%') # [DEBUG]
233 self
.vumeter_l
.set_fraction(pct
)
234 self
.vumeter_r
.set_fraction(pct
)
235 # Watching for feed loss during streaming:
237 if t
== Gst
.MessageType
.ERROR
:
238 err
, debug
= message
.parse_error()
239 if '(651)' not in debug
:
240 # The error is not a socket error.
241 self
.pipel
.stream_stop()
242 self
.build_filename(streamfailed
=True)
243 self
.create_backup_pipeline()
245 def on_stream_clicked(self
, widget
):
246 labelname1
= self
.stream_button
.get_label()
247 labelname2
= self
.test_button
.get_label()
248 if labelname1
== 'Stream':
249 if labelname2
!= 'Testing ...':
250 if self
.create_pipeline_instance():
251 self
.clean_entry_fields()
252 self
.pipel
.stream_play()
253 self
.stream_button
.set_label('ON AIR')
256 self
.testmode_info
.run()
257 self
.testmode_info
.hide()
258 elif labelname1
== 'ON AIR':
259 if self
.build_filename():
260 self
.pipel
.stream_stop()
261 self
.stream_button
.set_label('Stream')
263 def on_test_clicked(self
, widget
):
264 labelname
= self
.test_button
.get_label()
265 if labelname
== 'Set-up test':
266 if self
.create_pipeline_instance(feed
='test'):
267 self
.pipel
.stream_play()
268 self
.test_button
.set_label('Testing ...')
269 elif labelname
== 'Testing ...':
270 self
.pipel
.stream_stop()
271 self
.test_button
.set_label('Set-up test')
273 def on_tbutton_toggled(self
, tbutton
, name
):
275 running_cond
= (self
.stream_button
.get_label() == 'ON AIR' or
276 self
.test_button
.get_label() == 'Testing ...')
278 tbutton
.set_active(False)
279 ## if tbutton.active():
280 ## tbutton.set_active(True)
283 if tbutton
.get_active():
285 self
.cam2_tbutton
.set_active(False)
286 self
.cam3_tbutton
.set_active(False)
287 rtsp_address
= IP_1
+ PORT
289 self
.cam1_tbutton
.set_active(False)
290 self
.cam3_tbutton
.set_active(False)
291 rtsp_address
= IP_2
+ PORT
293 self
.cam1_tbutton
.set_active(False)
294 self
.cam2_tbutton
.set_active(False)
295 rtsp_address
= IP_3
+ PORT
297 def build_filename(self
, streamfailed
=False):
298 """Get text in entries, check if empty and apply formatting if needed."""
300 base
= self
.baseinfo_entry_label
.get_text()
301 speaker
= self
.speakerinfo_entry
.get_text()
302 speaker
= sep
.join(speaker
.split())
303 session
= self
.sessioninfo_entry
.get_text()
304 session
= sep
.join(session
.split())
305 raw_filename
= base
+ sep
+ speaker
+ sep
+ session
307 if speaker
and session
:
308 if len(raw_filename
) >= maxlen
:
309 offset
= len(raw_filename
) - maxlen
310 raw_filename
= raw_filename
[:-offset
]
312 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
314 self
.pipel
.set_filenames(raw_filename
,)
315 ## print('RAWFILENAME: ', raw_filename, ' <--') # [DEBUG]
317 self
.pipel
.set_filenames(raw_filename
, streamfailed
=True)
319 elif not streamfailed
:
320 self
.entryfield_info
.run()
321 self
.entryfield_info
.hide()
325 def clean_entry_fields(self
):
326 self
.speakerinfo_entry
.set_text('')
327 self
.sessioninfo_entry
.set_text('')
329 def iec_scale(self
, db
):
330 """Returns the meter deflection percentage given a db value."""
336 pct
= (db
+ 70.0) * 0.25
338 pct
= (db
+ 60.0) * 0.5 + 2.5
340 pct
= (db
+ 50.0) * 0.75 + 7.5
342 pct
= (db
+ 40.0) * 1.5 + 15.0
344 pct
= (db
+ 30.0) * 2.0 + 30.0
346 pct
= (db
+ 20.0) * 2.5 + 50.0
351 ## Use threading module to refresh the time elapsed sinc the begining of the stream??
352 def time_elapsed(self
, widget
):
353 if self
.pipel
.stream_get_state() == 'PLAYING':
357 if __name__
== "__main__":