Adding camera input selection (has to be connected to gstconf.py)
[libre-streamer.git] / stream_2016 / libre-streamer.py
1 #!/usr/bin/env python3.4
2
3 # This file is part of ABYSS.
4 #
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.
9 #
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.
14 #
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/>.
17 #
18 # Copyright (c) 2016 David Testé
19
20 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21 # TODO list:
22 # ----------
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 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
39
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 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
44
45 __author__ = 'David Testé'
46 __licence__ = 'GPLv3'
47 __version__ = 0.1
48 __maintainer__ = 'David Testé'
49 __email__ = 'soonum@gnu.org'
50 __status__ = 'Prototype'
51
52
53 import sys
54 from time import time, localtime, strftime
55
56 import gi
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
65
66 import gstconf
67
68 # Based on 2016 FSF's ELPHEL camera configuration
69 IP_1 = '192.168.48.2'
70 IP_2 = '192.168.48.3'
71 IP_3 = '192.168.48.4'
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.'
76
77 formatted_date = strftime('%Y_%m_%d', localtime())
78 metadata = {'speaker_name':'NC',
79 'session_title':'NC',
80 'organisation':'NC',}
81 start_time = 0
82
83
84 class Streamgui(object):
85
86
87 def __init__(self):
88
89 # Create the GUI
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)
104
105 self.videowidget = Gtk.DrawingArea()
106 self.videowidget.set_size_request(600, 400)
107
108
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)
123
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()
130
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')
135
136 self.cpuload_label = Gtk.Label('CPU load: ')
137 self.cpuload_value = Gtk.Label('NC')
138
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)
142
143 self.entryfield_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE,
144 text=ENTRYFIELD_TEXT,)
145 ##messagetype=Gtk.MessageType.WARNING,
146 ##Gtk.MessageType.INFO,)
147
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)
172
173 self.win.add(vbox)
174 self.win.set_position(Gtk.WindowPosition.CENTER)
175 self.win.show_all()
176
177 self.xid = self.videowidget.get_property('window').get_xid()
178
179 self.create_pipeline_instance()
180
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'
188
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()
194
195 def on_sync_message(self, bus, message):
196
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())
201
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:
212 t = message.type
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()
220
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')
230 start_time = time()
231 elif labelname == 'ON AIR':
232 if self.build_filename():
233 self.pipel.stream_stop()
234 self.stream_button.set_label('Stream')
235
236 def build_filename(self, streamfailed=False):
237 """Get text in entries, check if empty and apply formatting if needed."""
238 sep = '_'
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
245 maxlen = 70
246 if speaker and session:
247 if len(raw_filename) >= maxlen:
248 offset = len(raw_filename) - maxlen
249 raw_filename = raw_filename[:-offset]
250 if streamfailed:
251 self.pipel.set_filenames(raw_filename, streamfailed=True)
252 else:
253 self.pipel.set_filenames(raw_filename,)
254 print('RAWFILENAM: ', raw_filename, ' <--')
255 elif streamfailed:
256 self.pipel.set_filenames(raw_filename, streamfailed=True)
257 return True
258 elif not streamfailed:
259 self.entryfield_info.run()
260 self.entryfield_info.hide()
261 return False
262
263
264 def clean_entry_fields(self):
265 self.speakerinfo_entry.set_text('')
266 self.sessioninfo_entry.set_text('')
267
268 def iec_scale(self, db):
269 """Returns the meter deflection percentage given a db value."""
270 pct = 0.0
271
272 if db < -70.0:
273 pct = 0.0
274 elif db < -60.0:
275 pct = (db + 70.0) * 0.25
276 elif db < -50.0:
277 pct = (db + 60.0) * 0.5 + 2.5
278 elif db < -40.0:
279 pct = (db + 50.0) * 0.75 + 7.5
280 elif db < -30.0:
281 pct = (db + 40.0) * 1.5 + 15.0
282 elif db < -20.0:
283 pct = (db + 30.0) * 2.0 + 30.0
284 elif db < 0.0:
285 pct = (db + 20.0) * 2.5 + 50.0
286 else:
287 pct = 100.0
288 return pct / 100
289
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':
293 pass
294
295
296 if __name__ == "__main__":
297 Gst.init()
298 Streamgui()
299 Gtk.main()