Adding specific audio input (USB mixing desk)
[libre-streamer.git] / stream_2016 / libre-streamer.py
CommitLineData
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 50import sys
a8f9ff92 51from time import time, localtime, strftime
6fdd41d9
DT
52
53import gi
54gi.require_version('Gtk', '3.0')
55from gi.repository import Gtk
e9c7c8ad 56from gi.repository import Gdk
3a030c1f 57gi.require_version('Gst', '1.0')
6fdd41d9
DT
58from gi.repository import Gst
59from gi.repository import GdkX11
60from gi.repository import GstVideo
c5cb0627 61from gi.repository import GObject
6fdd41d9 62
c5cb0627
DT
63import gstconf
64
a8f9ff92 65formatted_date = strftime('%Y_%m_%d', localtime())
c5cb0627
DT
66metadata = {'speaker_name':'NC',
67 'session_title':'NC',
68 'organisation':'NC',}
69start_time = 0
6fdd41d9 70
3a9a5e09 71
6fdd41d9
DT
72class 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
259if __name__ == "__main__":
260 Gst.init()
261 Streamgui()
c5cb0627 262 Gtk.main()