e5924034090bbda34d4a7b0ab7c897c6d1da0394
[libre-streamer.git] / stream_2016 / abyss.py
1 #!/usr/bin/env python3.4
2 # -*- coding: utf-8 -*-
3
4 # This file is part of ABYSS.
5 # ABYSS Broadcast Your Streaming Successfully
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 # Copyright (c) 2016 David Testé
21
22 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23 # TODO list:
24 # ----------
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 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41
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 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
46
47 __author__ = 'David Testé'
48 __licence__ = 'GPLv3'
49 __version__ = 0.1
50 __maintainer__ = 'David Testé'
51 __email__ = 'soonum@gnu.org'
52 __status__ = 'Prototype'
53
54
55 import sys
56 from time import time, localtime, strftime
57
58 import gi
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
67
68 import gstconf
69
70 # Based on 2016 FSF's ELPHEL camera configuration
71 ##PORT = ':554'
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
78 ##rtsp_address = None
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',
84 'session_title':'NC',
85 'organisation':'NC',}
86 start_time = 0
87
88
89 class Streamgui(object):
90
91
92 def __init__(self):
93
94 # Initialize a pipeline
95 self.pipel = None
96
97 # Create the GUI
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)
112
113 self.videowidget = Gtk.DrawingArea()
114 self.videowidget.set_size_request(800, 600)
115
116
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)
131
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()
138
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)
145
146 self.cpuload_label = Gtk.Label('CPU load: ')
147 self.cpuload_value = Gtk.Label('NC')
148
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')
157
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,
165 text=TESTMODE_TEXT,)
166
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)
193
194 self.win.add(vbox)
195 self.win.set_position(Gtk.WindowPosition.CENTER)
196 self.win.show_all()
197
198 self.xid = self.videowidget.get_property('window').get_xid()
199
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)
206 return True
207
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()
213
214 def on_sync_message(self, bus, message):
215
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())
220
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:
231 t = message.type
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()
239
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')
249 start_time = time()
250 else:
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')
257
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')
267
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 ...')
272 ## if running_cond:
273 ## tbutton.set_active(False)
274 ## return
275 ##
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
289
290 def build_filename(self, streamfailed=False):
291 """Get text in entries, check if empty and apply formatting if needed."""
292 sep = '_'
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
299 maxlen = 70
300 if speaker and session:
301 if len(raw_filename) >= maxlen:
302 offset = len(raw_filename) - maxlen
303 raw_filename = raw_filename[:-offset]
304 if streamfailed:
305 self.pipel.set_filenames(raw_filename, streamfailed=True)
306 else:
307 self.pipel.set_filenames(raw_filename,)
308 ## print('RAWFILENAME: ', raw_filename, ' <--') # [DEBUG]
309 elif streamfailed:
310 self.pipel.set_filenames(raw_filename, streamfailed=True)
311 else:
312 self.pipel.set_filenames(raw_filename,)
313 return True
314 elif not streamfailed:
315 self.entryfield_info.run()
316 self.entryfield_info.hide()
317 return False
318
319
320 def clean_entry_fields(self):
321 self.speakerinfo_entry.set_text('')
322 self.sessioninfo_entry.set_text('')
323
324 def iec_scale(self, db):
325 """Returns the meter deflection percentage given a db value."""
326 pct = 0.0
327
328 if db < -70.0:
329 pct = 0.0
330 elif db < -60.0:
331 pct = (db + 70.0) * 0.25
332 elif db < -50.0:
333 pct = (db + 60.0) * 0.5 + 2.5
334 elif db < -40.0:
335 pct = (db + 50.0) * 0.75 + 7.5
336 elif db < -30.0:
337 pct = (db + 40.0) * 1.5 + 15.0
338 elif db < -20.0:
339 pct = (db + 30.0) * 2.0 + 30.0
340 elif db < 0.0:
341 pct = (db + 20.0) * 2.5 + 50.0
342 else:
343 pct = 100.0
344 return pct / 100
345
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':
349 pass
350
351
352 if __name__ == "__main__":
353 Gst.init()
354 Streamgui()
355 Gtk.main()