Testing pipeline completed.
[libre-streamer.git] / stream_2016 / libre-streamer.py
CommitLineData
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 55import sys
a8f9ff92 56from time import time, localtime, strftime
6fdd41d9
DT
57
58import gi
59gi.require_version('Gtk', '3.0')
60from gi.repository import Gtk
e9c7c8ad 61from gi.repository import Gdk
3a030c1f 62gi.require_version('Gst', '1.0')
6fdd41d9
DT
63from gi.repository import Gst
64from gi.repository import GdkX11
65from gi.repository import GstVideo
c5cb0627 66from gi.repository import GObject
6fdd41d9 67
c5cb0627
DT
68import gstconf
69
332e58df 70# Based on 2016 FSF's ELPHEL camera configuration
dfee4fc3 71PORT = ':554'
332e58df
DT
72IP_1 = '192.168.48.2'
73IP_2 = '192.168.48.3'
74IP_3 = '192.168.48.4'
75CAM1_IP1 = 'CAM_1: ' + IP_1
dfee4fc3
DT
76CAM2_IP2 = 'CAM_2: ' + IP_2
77CAM3_IP3 = 'CAM_3: ' + IP_3
78rtsp_address = None
79ENTRYFIELD_TEXT = 'Please fill both entry field to stop streaming.'
80CAMCHOICE_TEXT = 'Please choose a camera address.'
81TESTMODE_TEXT = 'Quit testing mode to switch to streaming mode.'
a8f9ff92 82formatted_date = strftime('%Y_%m_%d', localtime())
c5cb0627
DT
83metadata = {'speaker_name':'NC',
84 'session_title':'NC',
85 'organisation':'NC',}
86start_time = 0
6fdd41d9 87
3a9a5e09 88
6fdd41d9
DT
89class 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
357if __name__ == "__main__":
358 Gst.init()
359 Streamgui()
c5cb0627 360 Gtk.main()