Adding camera input selection (has to be connected to gstconf.py)
[libre-streamer.git] / stream_2016 / libre-streamer.py
CommitLineData
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 53import sys
a8f9ff92 54from time import time, localtime, strftime
6fdd41d9
DT
55
56import gi
57gi.require_version('Gtk', '3.0')
58from gi.repository import Gtk
e9c7c8ad 59from gi.repository import Gdk
3a030c1f 60gi.require_version('Gst', '1.0')
6fdd41d9
DT
61from gi.repository import Gst
62from gi.repository import GdkX11
63from gi.repository import GstVideo
c5cb0627 64from gi.repository import GObject
6fdd41d9 65
c5cb0627
DT
66import gstconf
67
332e58df
DT
68# Based on 2016 FSF's ELPHEL camera configuration
69IP_1 = '192.168.48.2'
70IP_2 = '192.168.48.3'
71IP_3 = '192.168.48.4'
72CAM1_IP1 = 'CAM_1: ' + IP_1
73CAM2_IP2 = 'CAM_1: ' + IP_2
74CAM3_IP3 = 'CAM_1: ' + IP_3
73c65412 75ENTRYFIELD_TEXT = 'Please fill both entry\nfield to stop streaming.'
332e58df 76
a8f9ff92 77formatted_date = strftime('%Y_%m_%d', localtime())
c5cb0627
DT
78metadata = {'speaker_name':'NC',
79 'session_title':'NC',
80 'organisation':'NC',}
81start_time = 0
6fdd41d9 82
3a9a5e09 83
6fdd41d9
DT
84class 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
296if __name__ == "__main__":
297 Gst.init()
298 Streamgui()
c5cb0627 299 Gtk.main()