Commit | Line | Data |
---|---|---|
6fdd41d9 DT |
1 | #!/usr/bin/env python3.4 |
2 | ||
3 | # This file is part of Libre-Streamer. | |
4 | # | |
5 | # Libre-Streamer 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 | # Libre-Streamer 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 Libre-Streamer. If not, see <http://www.gnu.org/licenses/>. | |
17 | # | |
18 | # Copyright (c) 2016 David Testé | |
19 | ||
20 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
21 | # TODO list: | |
22 | # ---------- | |
e9c7c8ad DT |
23 | # - Implement a method to switch to webcam feed if Elphel cam feed is lost |
24 | # - Display the Gst element 'videotestsrc', in case of failure of the whole pipeline | |
af8c02a4 DT |
25 | # - Add a checkbox to enable/disable options (storing/streaming - storing only - stream only - etc...) |
26 | # - Add a function to get the ip address of the camera automatically (see github.com/paulmilliken) | |
af8c02a4 | 27 | # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc) |
6fdd41d9 DT |
28 | # - Generate a log file during runtime. (e.g. this will let you know if the network configuration |
29 | # and the pipeline construction went well (or not)) | |
30 | # - Add an input source choice for the user (camera on IP or webcam) | |
c5cb0627 | 31 | # - Add a time counter |
c5cb0627 | 32 | # - Add a 'CPU load' widget |
6fdd41d9 | 33 | # - Add the FSF logo (need to do some pixel art) as an application icon |
af8c02a4 | 34 | # - Add the FSF logo inside the streamer use the 'textoverlay' method in ElementFactory.make() |
6fdd41d9 DT |
35 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
36 | ||
af8c02a4 DT |
37 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
38 | # INFO: run the following command in a terminal before launching libre-streamer to get a error log. | |
3a030c1f | 39 | # GST_DEBUG=4,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1 |
af8c02a4 DT |
40 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
41 | ||
42 | __author__ = 'David Testé' | |
43 | __licence__ = 'GPLv3' | |
44 | __version__ = 0.1 | |
45 | __maintainer__ = 'David Testé' | |
46 | __email__ = 'soonum@gnu.org' | |
47 | __status__ = 'Prototype' | |
48 | ||
49 | ||
6fdd41d9 | 50 | import sys |
a8f9ff92 | 51 | from time import time, localtime, strftime |
6fdd41d9 DT |
52 | |
53 | import gi | |
54 | gi.require_version('Gtk', '3.0') | |
55 | from gi.repository import Gtk | |
e9c7c8ad | 56 | from gi.repository import Gdk |
3a030c1f | 57 | gi.require_version('Gst', '1.0') |
6fdd41d9 DT |
58 | from gi.repository import Gst |
59 | from gi.repository import GdkX11 | |
60 | from gi.repository import GstVideo | |
c5cb0627 | 61 | from gi.repository import GObject |
6fdd41d9 | 62 | |
c5cb0627 DT |
63 | import gstconf |
64 | ||
a8f9ff92 | 65 | formatted_date = strftime('%Y_%m_%d', localtime()) |
c5cb0627 DT |
66 | metadata = {'speaker_name':'NC', |
67 | 'session_title':'NC', | |
68 | 'organisation':'NC',} | |
69 | start_time = 0 | |
6fdd41d9 | 70 | |
3a9a5e09 | 71 | |
6fdd41d9 DT |
72 | class Streamgui(object): |
73 | ||
74 | ||
75 | def __init__(self): | |
76 | ||
6fdd41d9 DT |
77 | # Create the GUI |
78 | self.win = Gtk.Window() | |
79 | self.win.set_title("Libre-Streamer") | |
80 | self.win.connect("delete_event", | |
81 | lambda w,e: Gtk.main_quit()) | |
82 | vbox = Gtk.VBox(False, 0) | |
c5cb0627 DT |
83 | vbox_labels = Gtk.VBox(False, 0) |
84 | vbox_entries = Gtk.VBox(False, 0) | |
85 | vbox_streaminfo = Gtk.VBox(False, 0) | |
86 | vbox_cpuinfo = Gtk.VBox(False, 0) | |
6fdd41d9 | 87 | hbox = Gtk.HBox(False, 0) |
e9c7c8ad | 88 | hbox_videoaudio = Gtk.HBox(False, 0) |
c5cb0627 DT |
89 | hbox_time = Gtk.HBox(False, 0) |
90 | ||
6fdd41d9 DT |
91 | self.videowidget = Gtk.DrawingArea() |
92 | self.videowidget.set_size_request(600, 400) | |
6fdd41d9 | 93 | |
3a9a5e09 DT |
94 | # True stereo feed has to be implemented: |
95 | self.vumeter_l = Gtk.ProgressBar() | |
96 | self.vumeter_l.set_orientation(Gtk.Orientation.VERTICAL) | |
97 | self.vumeter_l.set_inverted(True) | |
98 | self.vumeter_r = Gtk.ProgressBar() | |
99 | self.vumeter_r.set_orientation(Gtk.Orientation.VERTICAL) | |
100 | self.vumeter_r.set_inverted(True) | |
e9c7c8ad DT |
101 | ## Use CSS to modify the color of ProgressBar |
102 | ## color = Gdk.RGBA() | |
103 | ## Gdk.RGBA.parse(color, 'rgb(240,0,150)') | |
104 | ## print ("Color: ", color) | |
105 | ## self.vumeter.override_background_color(Gtk.StateFlags.NORMAL, color) | |
106 | ## self.vumeter.override_symbolic_color('bg_color', color) | |
107 | ## self.vumeter.override_symbolic_color('theme_bg_color', color) | |
108 | ||
c5cb0627 DT |
109 | self.baseinfo_label = Gtk.Label('Base info: ') |
110 | self.baseinfo_entry_label = Gtk.Label('LP_' + formatted_date) | |
111 | self.speakerinfo_label = Gtk.Label('Speaker name: ') | |
112 | self.speakerinfo_entry = Gtk.Entry() | |
113 | self.sessioninfo_label = Gtk.Label('Session name: ') | |
114 | self.sessioninfo_entry = Gtk.Entry() | |
c5cb0627 DT |
115 | |
116 | self.stream_button = Gtk.Button("Stream") | |
117 | self.stream_button.connect("clicked", self.on_stream_clicked) | |
118 | self.streamtime_label = Gtk.Label('Time elapsed ') | |
119 | self.streamtime_value = Gtk.Label('00:00:00') | |
120 | ||
e9c7c8ad | 121 | hbox_videoaudio.pack_start(self.videowidget, True, True, 0) |
3a9a5e09 DT |
122 | hbox_videoaudio.pack_start(self.vumeter_l, False, False, 3) |
123 | hbox_videoaudio.pack_start(self.vumeter_r, False, False, 3) | |
c5cb0627 DT |
124 | vbox_labels.pack_start(self.baseinfo_label, True, True, 0) |
125 | vbox_labels.pack_start(self.speakerinfo_label, True, True, 0) | |
126 | vbox_labels.pack_start(self.sessioninfo_label, True, True, 0) | |
c5cb0627 DT |
127 | vbox_entries.pack_start(self.baseinfo_entry_label, True, True, 0) |
128 | vbox_entries.pack_start(self.speakerinfo_entry, True, True, 0) | |
129 | vbox_entries.pack_start(self.sessioninfo_entry, True, True, 0) | |
c5cb0627 DT |
130 | vbox_streaminfo.pack_start(self.stream_button, False, True, 15) |
131 | hbox_time.pack_start(self.streamtime_label, False, False, 0) | |
132 | hbox_time.pack_start(self.streamtime_value, False, False, 0) | |
133 | vbox_streaminfo.pack_start(hbox_time, False, True, 0) | |
134 | hbox.pack_start(vbox_labels, False, False, 0) | |
135 | hbox.pack_start(vbox_entries, False, False, 0) | |
136 | hbox.pack_start(vbox_streaminfo, False, False, 0) | |
e9c7c8ad | 137 | vbox.pack_start(hbox_videoaudio, True, True, 0) |
6fdd41d9 | 138 | vbox.pack_start(hbox, False, True, 0) |
da450d89 | 139 | |
6fdd41d9 DT |
140 | self.win.add(vbox) |
141 | self.win.set_position(Gtk.WindowPosition.CENTER) | |
142 | self.win.show_all() | |
143 | ||
af8c02a4 | 144 | self.xid = self.videowidget.get_property('window').get_xid() |
3a030c1f | 145 | |
a8f9ff92 DT |
146 | self.create_pipeline_instance() |
147 | ||
148 | def create_pipeline_instance(self, feed='main'): | |
149 | """Creates pipeline instance and attaches it to GUI.""" | |
150 | self.pipel = gstconf.New_user_pipeline(feed) | |
c5cb0627 | 151 | bus = gstconf.get_gstreamer_bus() |
e9c7c8ad | 152 | bus.connect('sync-message::element', self.on_sync_message) |
3a9a5e09 | 153 | bus.connect('message', self.on_message) |
a8f9ff92 DT |
154 | # Try to use 'sync-message::element' instead of 'message' |
155 | ||
156 | def create_backup_pipeline(self): | |
157 | labelname = self.stream_button.get_label() | |
158 | if labelname == 'ON AIR': | |
159 | self.create_pipeline_instance(feed='backup') | |
160 | self.pipel.stream_play() | |
161 | ||
6fdd41d9 DT |
162 | def on_sync_message(self, bus, message): |
163 | ||
164 | if message.get_structure().get_name() == 'prepare-window-handle': | |
165 | imagesink = message.src | |
166 | imagesink.set_property('force-aspect-ratio', True) | |
167 | imagesink.set_window_handle(self.videowidget.get_property('window').get_xid()) | |
6fdd41d9 | 168 | |
3a9a5e09 DT |
169 | def on_message(self, bus, message): |
170 | # Getting the RMS audio level value: | |
e9c7c8ad DT |
171 | s = Gst.Message.get_structure(message) |
172 | if message.type == Gst.MessageType.ELEMENT: | |
173 | if str(Gst.Structure.get_name(s)) == 'level': | |
174 | pct = self.iec_scale(s.get_value('rms')[0]) | |
3a9a5e09 DT |
175 | ##print('Level value: ', pct, '%') # [DEBUG] |
176 | self.vumeter_l.set_fraction(pct) | |
177 | self.vumeter_r.set_fraction(pct) | |
178 | # Watching for feed loss during streaming: | |
179 | t = message.type | |
180 | if t == Gst.MessageType.ERROR: | |
a8f9ff92 DT |
181 | err, debug = message.parse_error() |
182 | if '(651)' not in debug: | |
183 | self.pipel.stream_stop() | |
184 | self.create_backup_pipeline() | |
3a9a5e09 | 185 | |
6fdd41d9 DT |
186 | def on_stream_clicked(self, widget): |
187 | ||
6fdd41d9 | 188 | labelname = self.stream_button.get_label() |
340ab727 | 189 | if labelname == 'Stream': |
a8f9ff92 DT |
190 | if self.pipel.feed == 'backup': |
191 | # Get back to main feed: | |
192 | self.create_pipeline_instance() | |
340ab727 | 193 | self.clean_entry_fields() |
c5cb0627 | 194 | self.pipel.stream_play() |
6fdd41d9 | 195 | self.stream_button.set_label('ON AIR') |
c5cb0627 | 196 | start_time = time() |
6fdd41d9 | 197 | elif labelname == 'ON AIR': |
c5cb0627 | 198 | self.pipel.stream_stop() |
6fdd41d9 | 199 | self.stream_button.set_label('Stream') |
e9c7c8ad | 200 | ## self.build_filename() |
a8f9ff92 | 201 | |
e9c7c8ad | 202 | ## In this state, this function freeze the streaming if the fields are NOT completed |
340ab727 DT |
203 | def build_filename(self): |
204 | """Get text in entries, check if empty and apply formatting if needed.""" | |
205 | sep = '_' | |
206 | base = self.baseinfo_entry_label.get_text() | |
207 | speaker = self.speakerinfo_entry.get_text() | |
208 | speaker = sep.join(speaker.split()) | |
209 | session = self.sessioninfo_entry.get_text() | |
210 | session = sep.join(session.split()) | |
211 | raw_filename = base + sep + speaker + sep + session | |
212 | maxlen = 70 | |
213 | has_all_fields = False | |
214 | while not has_all_fields: | |
215 | if speaker and session: | |
216 | if len(raw_filename) <= maxlen: | |
217 | has_all_fields = True | |
218 | else: | |
219 | offset = len(raw_filename) - maxlen | |
220 | raw_filename = raw_filename[:-offset] | |
221 | has_all_fields = True | |
222 | else: | |
223 | pass | |
224 | # One of the field is empty, open a dialogbox to ask for filling the field | |
225 | self.pipel.set_filenames(raw_filename) | |
226 | ||
227 | def clean_entry_fields(self): | |
228 | self.speakerinfo_entry.set_text('') | |
229 | self.sessioninfo_entry.set_text('') | |
6fdd41d9 | 230 | |
e9c7c8ad | 231 | def iec_scale(self, db): |
a8f9ff92 | 232 | """Returns the meter deflection percentage given a db value.""" |
e9c7c8ad DT |
233 | pct = 0.0 |
234 | ||
235 | if db < -70.0: | |
236 | pct = 0.0 | |
237 | elif db < -60.0: | |
238 | pct = (db + 70.0) * 0.25 | |
239 | elif db < -50.0: | |
240 | pct = (db + 60.0) * 0.5 + 2.5 | |
241 | elif db < -40.0: | |
242 | pct = (db + 50.0) * 0.75 + 7.5 | |
243 | elif db < -30.0: | |
244 | pct = (db + 40.0) * 1.5 + 15.0 | |
245 | elif db < -20.0: | |
246 | pct = (db + 30.0) * 2.0 + 30.0 | |
247 | elif db < 0.0: | |
248 | pct = (db + 20.0) * 2.5 + 50.0 | |
249 | else: | |
250 | pct = 100.0 | |
251 | return pct / 100 | |
252 | ||
253 | ## Use threading module to refresh the time elapsed sinc the begining of the stream?? | |
c5cb0627 DT |
254 | def time_elapsed(self, widget): |
255 | if self.pipel.stream_get_state() == 'PLAYING': | |
256 | pass | |
257 | ||
258 | ||
6fdd41d9 DT |
259 | if __name__ == "__main__": |
260 | Gst.init() | |
261 | Streamgui() | |
c5cb0627 | 262 | Gtk.main() |