Merge branch 'stream_2016_dev'
[libre-streamer.git] / abyss.py
CommitLineData
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
55import sys
56from time import time, localtime, strftime
57
58import gi
59gi.require_version('Gtk', '3.0')
60from gi.repository import Gtk
61from gi.repository import Gdk
62gi.require_version('Gst', '1.0')
63from gi.repository import Gst
64from gi.repository import GdkX11
65from gi.repository import GstVideo
66from gi.repository import GObject
67
68import 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
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.'
82formatted_date = strftime('%Y_%m_%d', localtime())
83metadata = {'speaker_name':'NC',
84 'session_title':'NC',
85 'organisation':'NC',}
86start_time = 0
87
88
89class 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
352if __name__ == "__main__":
353 Gst.init()
354 Streamgui()
355 Gtk.main()