Commit | Line | Data |
---|---|---|
6fdd41d9 | 1 | #!/usr/bin/env python3.4 |
dfee4fc3 | 2 | # -*- coding: utf-8 -*- |
6fdd41d9 | 3 | |
332e58df | 4 | # This file is part of ABYSS. |
dfee4fc3 | 5 | # ABYSS Broadcast Your Streaming Successfully |
6fdd41d9 | 6 | # |
332e58df | 7 | # ABYSS is free software: you can redistribute it and/or modify |
6fdd41d9 DT |
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 | # | |
332e58df | 12 | # ABYSS is distributed in the hope that it will be useful, |
6fdd41d9 DT |
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 | |
332e58df | 18 | # along with ABYSS. If not, see <http://www.gnu.org/licenses/>. |
6fdd41d9 DT |
19 | # |
20 | # Copyright (c) 2016 David Testé | |
21 | ||
22 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
23 | # TODO list: | |
24 | # ---------- | |
e9c7c8ad | 25 | # - Implement a method to switch to webcam feed if Elphel cam feed is lost |
4fc18e8b DT |
26 | # --> Use ping by opening Telnet connexion every 2 seconds (if it fails, then switch to webcam) |
27 | # --> Has to be threaded | |
af8c02a4 DT |
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) | |
af8c02a4 | 30 | # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc) |
4fc18e8b | 31 | # --> Taken care in FAI building |
6fdd41d9 DT |
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) | |
4fc18e8b DT |
35 | # - Add a time counter |
36 | # --> Has to be threaded | |
c5cb0627 | 37 | # - Add a 'CPU load' widget |
6fdd41d9 | 38 | # - Add the FSF logo (need to do some pixel art) as an application icon |
af8c02a4 | 39 | # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make() |
6fdd41d9 DT |
40 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
41 | ||
af8c02a4 DT |
42 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
43 | # INFO: run the following command in a terminal before launching libre-streamer to get a error log. | |
3a030c1f | 44 | # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1 |
af8c02a4 DT |
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 | ||
6fdd41d9 | 55 | import sys |
a8f9ff92 | 56 | from time import time, localtime, strftime |
6fdd41d9 DT |
57 | |
58 | import gi | |
59 | gi.require_version('Gtk', '3.0') | |
60 | from gi.repository import Gtk | |
e9c7c8ad | 61 | from gi.repository import Gdk |
3a030c1f | 62 | gi.require_version('Gst', '1.0') |
6fdd41d9 DT |
63 | from gi.repository import Gst |
64 | from gi.repository import GdkX11 | |
65 | from gi.repository import GstVideo | |
c5cb0627 | 66 | from gi.repository import GObject |
6fdd41d9 | 67 | |
c5cb0627 DT |
68 | import gstconf |
69 | ||
332e58df | 70 | # Based on 2016 FSF's ELPHEL camera configuration |
dfee4fc3 | 71 | PORT = ':554' |
332e58df DT |
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 | |
dfee4fc3 DT |
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.' | |
a8f9ff92 | 82 | formatted_date = strftime('%Y_%m_%d', localtime()) |
c5cb0627 DT |
83 | metadata = {'speaker_name':'NC', |
84 | 'session_title':'NC', | |
85 | 'organisation':'NC',} | |
86 | start_time = 0 | |
6fdd41d9 | 87 | |
3a9a5e09 | 88 | |
6fdd41d9 DT |
89 | class Streamgui(object): |
90 | ||
91 | ||
92 | def __init__(self): | |
93 | ||
dfee4fc3 DT |
94 | # Initialize a pipeline |
95 | self.pipel = None | |
96 | ||
6fdd41d9 DT |
97 | # Create the GUI |
98 | self.win = Gtk.Window() | |
332e58df | 99 | self.win.set_title("ABYSS") |
6fdd41d9 DT |
100 | self.win.connect("delete_event", |
101 | lambda w,e: Gtk.main_quit()) | |
73c65412 | 102 | ## self.win.fullscreen() |
6fdd41d9 | 103 | vbox = Gtk.VBox(False, 0) |
c5cb0627 DT |
104 | vbox_labels = Gtk.VBox(False, 0) |
105 | vbox_entries = Gtk.VBox(False, 0) | |
dfee4fc3 DT |
106 | vbox_streaminfo = Gtk.VBox(True, 0) |
107 | vbox_tbuttongrp = Gtk.VBox(False, 0) | |
73c65412 | 108 | hbox = Gtk.HBox(False, 30) |
e9c7c8ad | 109 | hbox_videoaudio = Gtk.HBox(False, 0) |
c5cb0627 | 110 | hbox_time = Gtk.HBox(False, 0) |
dfee4fc3 | 111 | hbox_cpu = Gtk.HBox(False, 0) |
c5cb0627 | 112 | |
6fdd41d9 | 113 | self.videowidget = Gtk.DrawingArea() |
dfee4fc3 | 114 | self.videowidget.set_size_request(800, 600) |
6fdd41d9 | 115 | |
73c65412 | 116 | |
3a9a5e09 DT |
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) | |
e9c7c8ad DT |
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 | ||
c5cb0627 DT |
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() | |
c5cb0627 | 138 | |
dfee4fc3 DT |
139 | self.stream_button = Gtk.Button('Stream') |
140 | self.stream_button.connect('clicked', self.on_stream_clicked) | |
c5cb0627 DT |
141 | self.streamtime_label = Gtk.Label('Time elapsed ') |
142 | self.streamtime_value = Gtk.Label('00:00:00') | |
dfee4fc3 DT |
143 | self.test_button = Gtk.Button('Set-up test') |
144 | self.test_button.connect('clicked', self.on_test_clicked) | |
73c65412 DT |
145 | |
146 | self.cpuload_label = Gtk.Label('CPU load: ') | |
147 | self.cpuload_value = Gtk.Label('NC') | |
148 | ||
dfee4fc3 DT |
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') | |
332e58df | 157 | |
73c65412 DT |
158 | self.entryfield_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, |
159 | text=ENTRYFIELD_TEXT,) | |
160 | ##messagetype=Gtk.MessageType.WARNING, | |
161 | ##Gtk.MessageType.INFO,) | |
dfee4fc3 DT |
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,) | |
c5cb0627 | 166 | |
e9c7c8ad | 167 | hbox_videoaudio.pack_start(self.videowidget, True, True, 0) |
3a9a5e09 DT |
168 | hbox_videoaudio.pack_start(self.vumeter_l, False, False, 3) |
169 | hbox_videoaudio.pack_start(self.vumeter_r, False, False, 3) | |
c5cb0627 DT |
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) | |
c5cb0627 DT |
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) | |
c5cb0627 DT |
176 | hbox_time.pack_start(self.streamtime_label, False, False, 0) |
177 | hbox_time.pack_start(self.streamtime_value, False, False, 0) | |
dfee4fc3 DT |
178 | hbox_cpu.pack_start(self.cpuload_label, False, False, 0) |
179 | hbox_cpu.pack_start(self.cpuload_value, False, False, 0) | |
c5cb0627 | 180 | vbox_streaminfo.pack_start(hbox_time, False, True, 0) |
dfee4fc3 DT |
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) | |
c5cb0627 DT |
185 | hbox.pack_start(vbox_labels, False, False, 0) |
186 | hbox.pack_start(vbox_entries, False, False, 0) | |
dfee4fc3 DT |
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) | |
c5cb0627 | 190 | hbox.pack_start(vbox_streaminfo, False, False, 0) |
e9c7c8ad | 191 | vbox.pack_start(hbox_videoaudio, True, True, 0) |
6fdd41d9 | 192 | vbox.pack_start(hbox, False, True, 0) |
da450d89 | 193 | |
6fdd41d9 DT |
194 | self.win.add(vbox) |
195 | self.win.set_position(Gtk.WindowPosition.CENTER) | |
196 | self.win.show_all() | |
197 | ||
af8c02a4 | 198 | self.xid = self.videowidget.get_property('window').get_xid() |
3a030c1f | 199 | |
a8f9ff92 DT |
200 | def create_pipeline_instance(self, feed='main'): |
201 | """Creates pipeline instance and attaches it to GUI.""" | |
dfee4fc3 DT |
202 | if rtsp_address: |
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) | |
207 | return True | |
208 | else: | |
209 | self.camchoice_info.run() | |
210 | self.camchoice_info.hide() | |
211 | return False | |
a8f9ff92 DT |
212 | |
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() | |
218 | ||
6fdd41d9 DT |
219 | def on_sync_message(self, bus, message): |
220 | ||
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()) | |
6fdd41d9 | 225 | |
3a9a5e09 DT |
226 | def on_message(self, bus, message): |
227 | # Getting the RMS audio level value: | |
e9c7c8ad DT |
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]) | |
3a9a5e09 DT |
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: | |
236 | t = message.type | |
237 | if t == Gst.MessageType.ERROR: | |
a8f9ff92 DT |
238 | err, debug = message.parse_error() |
239 | if '(651)' not in debug: | |
73c65412 | 240 | # The error is not a socket error. |
a8f9ff92 | 241 | self.pipel.stream_stop() |
4fc18e8b | 242 | self.build_filename(streamfailed=True) |
a8f9ff92 | 243 | self.create_backup_pipeline() |
3a9a5e09 | 244 | |
6fdd41d9 | 245 | def on_stream_clicked(self, widget): |
dfee4fc3 DT |
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') | |
254 | start_time = time() | |
255 | else: | |
256 | self.testmode_info.run() | |
257 | self.testmode_info.hide() | |
258 | elif labelname1 == 'ON AIR': | |
73c65412 DT |
259 | if self.build_filename(): |
260 | self.pipel.stream_stop() | |
261 | self.stream_button.set_label('Stream') | |
262 | ||
dfee4fc3 DT |
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') | |
272 | ||
273 | def on_tbutton_toggled(self, tbutton, name): | |
274 | global rtsp_address | |
275 | running_cond = (self.stream_button.get_label() == 'ON AIR' or | |
276 | self.test_button.get_label() == 'Testing ...') | |
277 | if running_cond: | |
278 | tbutton.set_active(False) | |
279 | ## if tbutton.active(): | |
280 | ## tbutton.set_active(True) | |
281 | return | |
282 | ||
283 | if tbutton.get_active(): | |
284 | if name == 'cam1': | |
285 | self.cam2_tbutton.set_active(False) | |
286 | self.cam3_tbutton.set_active(False) | |
287 | rtsp_address = IP_1 + PORT | |
288 | elif name == 'cam2': | |
289 | self.cam1_tbutton.set_active(False) | |
290 | self.cam3_tbutton.set_active(False) | |
291 | rtsp_address = IP_2 + PORT | |
292 | elif name == 'cam3': | |
293 | self.cam1_tbutton.set_active(False) | |
294 | self.cam2_tbutton.set_active(False) | |
295 | rtsp_address = IP_3 + PORT | |
296 | ||
4fc18e8b | 297 | def build_filename(self, streamfailed=False): |
340ab727 DT |
298 | """Get text in entries, check if empty and apply formatting if needed.""" |
299 | sep = '_' | |
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 | |
306 | maxlen = 70 | |
73c65412 DT |
307 | if speaker and session: |
308 | if len(raw_filename) >= maxlen: | |
309 | offset = len(raw_filename) - maxlen | |
310 | raw_filename = raw_filename[:-offset] | |
4fc18e8b DT |
311 | if streamfailed: |
312 | self.pipel.set_filenames(raw_filename, streamfailed=True) | |
313 | else: | |
314 | self.pipel.set_filenames(raw_filename,) | |
dfee4fc3 | 315 | ## print('RAWFILENAME: ', raw_filename, ' <--') # [DEBUG] |
4fc18e8b DT |
316 | elif streamfailed: |
317 | self.pipel.set_filenames(raw_filename, streamfailed=True) | |
73c65412 | 318 | return True |
4fc18e8b | 319 | elif not streamfailed: |
73c65412 DT |
320 | self.entryfield_info.run() |
321 | self.entryfield_info.hide() | |
322 | return False | |
4fc18e8b DT |
323 | |
324 | ||
340ab727 DT |
325 | def clean_entry_fields(self): |
326 | self.speakerinfo_entry.set_text('') | |
327 | self.sessioninfo_entry.set_text('') | |
6fdd41d9 | 328 | |
e9c7c8ad | 329 | def iec_scale(self, db): |
a8f9ff92 | 330 | """Returns the meter deflection percentage given a db value.""" |
e9c7c8ad DT |
331 | pct = 0.0 |
332 | ||
333 | if db < -70.0: | |
334 | pct = 0.0 | |
335 | elif db < -60.0: | |
336 | pct = (db + 70.0) * 0.25 | |
337 | elif db < -50.0: | |
338 | pct = (db + 60.0) * 0.5 + 2.5 | |
339 | elif db < -40.0: | |
340 | pct = (db + 50.0) * 0.75 + 7.5 | |
341 | elif db < -30.0: | |
342 | pct = (db + 40.0) * 1.5 + 15.0 | |
343 | elif db < -20.0: | |
344 | pct = (db + 30.0) * 2.0 + 30.0 | |
345 | elif db < 0.0: | |
346 | pct = (db + 20.0) * 2.5 + 50.0 | |
347 | else: | |
348 | pct = 100.0 | |
349 | return pct / 100 | |
350 | ||
351 | ## Use threading module to refresh the time elapsed sinc the begining of the stream?? | |
c5cb0627 DT |
352 | def time_elapsed(self, widget): |
353 | if self.pipel.stream_get_state() == 'PLAYING': | |
354 | pass | |
355 | ||
356 | ||
6fdd41d9 DT |
357 | if __name__ == "__main__": |
358 | Gst.init() | |
359 | Streamgui() | |
c5cb0627 | 360 | Gtk.main() |