Commit | Line | Data |
---|---|---|
969dd837 DT |
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()) | |
daa5109e | 102 | ## self.win.fullscreen() |
969dd837 DT |
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) | |
6bb57e06 DT |
311 | else: |
312 | self.pipel.set_filenames(raw_filename,) | |
969dd837 DT |
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() |