Commit | Line | Data |
---|---|---|
6fdd41d9 DT |
1 | #!/usr/bin/env python3.4 |
2 | ||
332e58df | 3 | # This file is part of ABYSS. |
6fdd41d9 | 4 | # |
332e58df | 5 | # ABYSS is free software: you can redistribute it and/or modify |
6fdd41d9 DT |
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 | # | |
332e58df | 10 | # ABYSS is distributed in the hope that it will be useful, |
6fdd41d9 DT |
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 | |
332e58df | 16 | # along with ABYSS. If not, see <http://www.gnu.org/licenses/>. |
6fdd41d9 DT |
17 | # |
18 | # Copyright (c) 2016 David Testé | |
19 | ||
20 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
21 | # TODO list: | |
22 | # ---------- | |
e9c7c8ad | 23 | # - Implement a method to switch to webcam feed if Elphel cam feed is lost |
4fc18e8b DT |
24 | # --> Use ping by opening Telnet connexion every 2 seconds (if it fails, then switch to webcam) |
25 | # --> Has to be threaded | |
af8c02a4 DT |
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) | |
af8c02a4 | 28 | # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc) |
4fc18e8b | 29 | # --> Taken care in FAI building |
6fdd41d9 DT |
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) | |
4fc18e8b DT |
33 | # - Add a time counter |
34 | # --> Has to be threaded | |
c5cb0627 | 35 | # - Add a 'CPU load' widget |
6fdd41d9 | 36 | # - Add the FSF logo (need to do some pixel art) as an application icon |
af8c02a4 | 37 | # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make() |
6fdd41d9 DT |
38 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
39 | ||
af8c02a4 DT |
40 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
41 | # INFO: run the following command in a terminal before launching libre-streamer to get a error log. | |
3a030c1f | 42 | # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1 |
af8c02a4 DT |
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 | ||
6fdd41d9 | 53 | import sys |
a8f9ff92 | 54 | from time import time, localtime, strftime |
6fdd41d9 DT |
55 | |
56 | import gi | |
57 | gi.require_version('Gtk', '3.0') | |
58 | from gi.repository import Gtk | |
e9c7c8ad | 59 | from gi.repository import Gdk |
3a030c1f | 60 | gi.require_version('Gst', '1.0') |
6fdd41d9 DT |
61 | from gi.repository import Gst |
62 | from gi.repository import GdkX11 | |
63 | from gi.repository import GstVideo | |
c5cb0627 | 64 | from gi.repository import GObject |
6fdd41d9 | 65 | |
c5cb0627 DT |
66 | import gstconf |
67 | ||
332e58df DT |
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 | |
73c65412 | 75 | ENTRYFIELD_TEXT = 'Please fill both entry\nfield to stop streaming.' |
332e58df | 76 | |
a8f9ff92 | 77 | formatted_date = strftime('%Y_%m_%d', localtime()) |
c5cb0627 DT |
78 | metadata = {'speaker_name':'NC', |
79 | 'session_title':'NC', | |
80 | 'organisation':'NC',} | |
81 | start_time = 0 | |
6fdd41d9 | 82 | |
3a9a5e09 | 83 | |
6fdd41d9 DT |
84 | class Streamgui(object): |
85 | ||
86 | ||
87 | def __init__(self): | |
88 | ||
6fdd41d9 DT |
89 | # Create the GUI |
90 | self.win = Gtk.Window() | |
332e58df | 91 | self.win.set_title("ABYSS") |
6fdd41d9 DT |
92 | self.win.connect("delete_event", |
93 | lambda w,e: Gtk.main_quit()) | |
73c65412 | 94 | ## self.win.fullscreen() |
6fdd41d9 | 95 | vbox = Gtk.VBox(False, 0) |
c5cb0627 DT |
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) | |
332e58df | 100 | vbox_rbuttongrp = Gtk.VBox(False, 0) |
73c65412 | 101 | hbox = Gtk.HBox(False, 30) |
e9c7c8ad | 102 | hbox_videoaudio = Gtk.HBox(False, 0) |
c5cb0627 DT |
103 | hbox_time = Gtk.HBox(False, 0) |
104 | ||
6fdd41d9 DT |
105 | self.videowidget = Gtk.DrawingArea() |
106 | self.videowidget.set_size_request(600, 400) | |
6fdd41d9 | 107 | |
73c65412 | 108 | |
3a9a5e09 DT |
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) | |
e9c7c8ad DT |
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 | ||
c5cb0627 DT |
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() | |
c5cb0627 DT |
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') | |
73c65412 DT |
135 | |
136 | self.cpuload_label = Gtk.Label('CPU load: ') | |
137 | self.cpuload_value = Gtk.Label('NC') | |
138 | ||
332e58df DT |
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 | ||
73c65412 DT |
143 | self.entryfield_info = Gtk.MessageDialog(buttons=Gtk.ButtonsType.CLOSE, |
144 | text=ENTRYFIELD_TEXT,) | |
145 | ##messagetype=Gtk.MessageType.WARNING, | |
146 | ##Gtk.MessageType.INFO,) | |
c5cb0627 | 147 | |
e9c7c8ad | 148 | hbox_videoaudio.pack_start(self.videowidget, True, True, 0) |
3a9a5e09 DT |
149 | hbox_videoaudio.pack_start(self.vumeter_l, False, False, 3) |
150 | hbox_videoaudio.pack_start(self.vumeter_r, False, False, 3) | |
c5cb0627 DT |
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) | |
c5cb0627 DT |
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) | |
c5cb0627 DT |
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) | |
332e58df DT |
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) | |
c5cb0627 DT |
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) | |
73c65412 DT |
167 | hbox.pack_start(self.cpuload_label, False, False, 0) |
168 | hbox.pack_start(self.cpuload_value, False, False, 0) | |
332e58df | 169 | hbox.pack_start(vbox_rbuttongrp, False, False, 0) |
e9c7c8ad | 170 | vbox.pack_start(hbox_videoaudio, True, True, 0) |
6fdd41d9 | 171 | vbox.pack_start(hbox, False, True, 0) |
da450d89 | 172 | |
6fdd41d9 DT |
173 | self.win.add(vbox) |
174 | self.win.set_position(Gtk.WindowPosition.CENTER) | |
175 | self.win.show_all() | |
176 | ||
af8c02a4 | 177 | self.xid = self.videowidget.get_property('window').get_xid() |
3a030c1f | 178 | |
a8f9ff92 DT |
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) | |
c5cb0627 | 184 | bus = gstconf.get_gstreamer_bus() |
e9c7c8ad | 185 | bus.connect('sync-message::element', self.on_sync_message) |
3a9a5e09 | 186 | bus.connect('message', self.on_message) |
a8f9ff92 DT |
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 | ||
6fdd41d9 DT |
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()) | |
6fdd41d9 | 201 | |
3a9a5e09 DT |
202 | def on_message(self, bus, message): |
203 | # Getting the RMS audio level value: | |
e9c7c8ad DT |
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]) | |
3a9a5e09 DT |
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: | |
a8f9ff92 DT |
214 | err, debug = message.parse_error() |
215 | if '(651)' not in debug: | |
73c65412 | 216 | # The error is not a socket error. |
a8f9ff92 | 217 | self.pipel.stream_stop() |
4fc18e8b | 218 | self.build_filename(streamfailed=True) |
a8f9ff92 | 219 | self.create_backup_pipeline() |
3a9a5e09 | 220 | |
6fdd41d9 | 221 | def on_stream_clicked(self, widget): |
6fdd41d9 | 222 | labelname = self.stream_button.get_label() |
340ab727 | 223 | if labelname == 'Stream': |
a8f9ff92 DT |
224 | if self.pipel.feed == 'backup': |
225 | # Get back to main feed: | |
226 | self.create_pipeline_instance() | |
340ab727 | 227 | self.clean_entry_fields() |
c5cb0627 | 228 | self.pipel.stream_play() |
6fdd41d9 | 229 | self.stream_button.set_label('ON AIR') |
c5cb0627 | 230 | start_time = time() |
6fdd41d9 | 231 | elif labelname == 'ON AIR': |
73c65412 DT |
232 | if self.build_filename(): |
233 | self.pipel.stream_stop() | |
234 | self.stream_button.set_label('Stream') | |
235 | ||
4fc18e8b | 236 | def build_filename(self, streamfailed=False): |
340ab727 DT |
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 | |
73c65412 DT |
246 | if speaker and session: |
247 | if len(raw_filename) >= maxlen: | |
248 | offset = len(raw_filename) - maxlen | |
249 | raw_filename = raw_filename[:-offset] | |
4fc18e8b DT |
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) | |
73c65412 | 257 | return True |
4fc18e8b | 258 | elif not streamfailed: |
73c65412 DT |
259 | self.entryfield_info.run() |
260 | self.entryfield_info.hide() | |
261 | return False | |
4fc18e8b DT |
262 | |
263 | ||
340ab727 DT |
264 | def clean_entry_fields(self): |
265 | self.speakerinfo_entry.set_text('') | |
266 | self.sessioninfo_entry.set_text('') | |
6fdd41d9 | 267 | |
e9c7c8ad | 268 | def iec_scale(self, db): |
a8f9ff92 | 269 | """Returns the meter deflection percentage given a db value.""" |
e9c7c8ad DT |
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?? | |
c5cb0627 DT |
291 | def time_elapsed(self, widget): |
292 | if self.pipel.stream_get_state() == 'PLAYING': | |
293 | pass | |
294 | ||
295 | ||
6fdd41d9 DT |
296 | if __name__ == "__main__": |
297 | Gst.init() | |
298 | Streamgui() | |
c5cb0627 | 299 | Gtk.main() |