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 | # ---------- | |
af8c02a4 DT |
23 | # - Add a form to fill before start streaming (conf title, name, etc...) |
24 | # - Add a checkbox to enable/disable options (storing/streaming - storing only - stream only - etc...) | |
25 | # - Add a function to get the ip address of the camera automatically (see github.com/paulmilliken) | |
6fdd41d9 | 26 | # - Create a module for the pipeline construction section to clarify the code |
af8c02a4 DT |
27 | # - Implement 2016 edition pipeline, see file 'gstream_pipeline_by_quidam' |
28 | # - Create a module for the network configuration (fan/cpu, ifconfig, stream server,etc) | |
6fdd41d9 DT |
29 | # - Generate a log file during runtime. (e.g. this will let you know if the network configuration |
30 | # and the pipeline construction went well (or not)) | |
31 | # - Add an input source choice for the user (camera on IP or webcam) | |
32 | # - Add a VU-meter to check if audio feed is emitting signal | |
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. | |
39 | # GST_DEBUG=3,python:5,gnl*:5 ./libre-streamer.py | tee -a log 2>&1 | |
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 DT |
50 | import sys |
51 | ||
52 | import gi | |
53 | gi.require_version('Gtk', '3.0') | |
54 | from gi.repository import Gtk | |
55 | from gi.repository import Gst | |
56 | from gi.repository import GdkX11 | |
57 | from gi.repository import GstVideo | |
58 | ||
6fdd41d9 DT |
59 | |
60 | class Streamgui(object): | |
61 | ||
62 | ||
63 | def __init__(self): | |
64 | ||
65 | self.multimedia_file="" | |
66 | # Create the global pipeline (might wanna use a general bin instead) | |
67 | self.pipel = self.constructpipeline() | |
68 | # Create the GUI | |
69 | self.win = Gtk.Window() | |
70 | self.win.set_title("Libre-Streamer") | |
71 | self.win.connect("delete_event", | |
72 | lambda w,e: Gtk.main_quit()) | |
73 | vbox = Gtk.VBox(False, 0) | |
74 | hbox = Gtk.HBox(False, 0) | |
75 | self.load_file = Gtk.FileChooserButton("Choose Audio File") | |
76 | self.stream_button = Gtk.Button("Stream") | |
77 | self.videowidget = Gtk.DrawingArea() | |
78 | self.videowidget.set_size_request(600, 400) | |
79 | self.load_file.connect("selection-changed", self.on_file_selected) | |
80 | self.stream_button.connect("clicked", self.on_stream_clicked) | |
81 | ||
82 | hbox.pack_start(self.stream_button, False, True, 0) | |
83 | vbox.pack_start(self.load_file, False, True, 0) | |
84 | vbox.pack_start(self.videowidget, True, True, 0) | |
85 | vbox.pack_start(hbox, False, True, 0) | |
86 | self.win.add(vbox) | |
87 | self.win.set_position(Gtk.WindowPosition.CENTER) | |
88 | self.win.show_all() | |
89 | ||
af8c02a4 DT |
90 | self.xid = self.videowidget.get_property('window').get_xid() |
91 | self.connectsignals() | |
92 | ||
93 | def connectsignals(self): | |
94 | """Connects signals with the methods""" | |
6fdd41d9 DT |
95 | bus = self.pipel.get_bus() |
96 | bus.add_signal_watch() | |
97 | bus.enable_sync_message_emission() | |
98 | # Used to get messages that GStreamer emits. | |
99 | bus.connect("message", self.on_message) | |
100 | # Used for connecting video to your application. | |
101 | bus.connect("sync-message::element", self.on_sync_message) | |
af8c02a4 DT |
102 | # Connect the rtpdepay signal |
103 | self.videosrc.connect("pad-added", self.on_pad_added_to_rtspsrc) | |
104 | if self.decodebin: | |
105 | self.decodebin.connect("pad-added", self.on_pad_added_to_decodebin) | |
106 | ## elif self.jpegdec: | |
107 | ## self.jpegdec.connect("pad-added", self.on_pad_added_to_jpegdec) | |
108 | ||
109 | def on_pad_added_to_rtspsrc(self, rtspsrc, pad): | |
110 | ## if self.decodebin: | |
111 | ## sinkpad = self.decodebin.get_static_pad('sink') | |
112 | ## elif self.rtpjpegdepay: | |
113 | ## sinkpad = self.rtpjpegdepay.get_static_pad('sink') | |
114 | sinkpad = self.queuev_1.get_static_pad('sink') | |
115 | pad.link(sinkpad) | |
116 | print('[DEBUG] rtspsrc LINKED') | |
6fdd41d9 | 117 | |
af8c02a4 DT |
118 | def on_pad_added_to_decodebin(self, decodebin, pad): |
119 | screen_sinkpad = self.screensink.get_static_pad('sink') | |
120 | pad.link(screen_sinkpad) | |
121 | print('[DEBUG] decodebin LINKED') | |
6fdd41d9 | 122 | |
af8c02a4 DT |
123 | def on_pad_added_to_jpegdec(self, jpegdec, pad): |
124 | screen_sinkpad = self.screensink.get_static_pad('sink') | |
125 | pad.link(screen_sinkpad) | |
126 | print('[DEBUG] decodebin LINKED') | |
127 | ||
128 | def constructpipeline (self): | |
6fdd41d9 | 129 | """Add and link elements in a GStreamer pipeline""" |
6fdd41d9 DT |
130 | # Create the pipelines instance. |
131 | self.streampipe = Gst.Pipeline() | |
6fdd41d9 DT |
132 | |
133 | # Define pipeline elements. | |
af8c02a4 DT |
134 | self.videosrc = Gst.ElementFactory.make('rtspsrc', 'videosrc') |
135 | self.videosrc.set_property('location', 'rtsp://192.168.48.2:554') | |
136 | self.videosrc.set_property('latency', 100) | |
137 | ||
138 | self.decodebin = Gst.ElementFactory.make('decodebin', 'decodebin') | |
139 | ||
140 | ## Video source for testing purpose: | |
141 | ## self.videosrc = Gst.ElementFactory.make('videotestsrc', 'videosrc') | |
142 | self.rtpjpegdepay = Gst.ElementFactory.make('rtpjpegdepay', 'rtpjpegdepay') | |
143 | self.jpegdec = Gst.ElementFactory.make('jpegdec', 'jpegdec') | |
144 | self.jpegdec.set_property('max-errors', -1) | |
145 | self.mkvmux = Gst.ElementFactory.make('matroskamux', 'mkvmux') | |
146 | self.tee_rawvideo = Gst.ElementFactory.make('tee', 'tee_rawvideo') | |
147 | self.queuev_1 = Gst.ElementFactory.make('queue', 'queuev_1') | |
148 | self.queuev_2 = Gst.ElementFactory.make('queue', 'queuev_2') | |
149 | self.queuev_3 = Gst.ElementFactory.make('queue', 'queuev_3') | |
150 | self.queuev_4 = Gst.ElementFactory.make('queue', 'queuev_4') | |
151 | ||
152 | self.disksink_rawvideo = Gst.ElementFactory.make('filesink') | |
153 | #[TO DO]: File location has to be defined | |
154 | self.disksink_rawvideo.set_property('location', 'popo_rawvideo') | |
155 | self.screensink = Gst.ElementFactory.make('xvimagesink', 'screensink') | |
156 | ||
157 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | |
158 | # IMPORTANT: | |
159 | # for 'webmmux' element streamable=True MUST be set! | |
160 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | |
161 | ||
162 | # Elements to test: | |
163 | #--------------------------------------------------------------------------- | |
164 | self.audiosrc = Gst.ElementFactory.make('pulsesrc', 'audiosrc') | |
165 | self.vorbisenc = Gst.ElementFactory.make('vorbisenc', 'vorbisenc') | |
166 | ## scaling_caps = Gst.Caps('video/x-raw, width=640, height=360') | |
167 | self.scaling = Gst.ElementFactory.make('videoscale', 'scaling') | |
168 | self.vp8enc = Gst.ElementFactory.make('vp8enc', 'vp8enc') | |
169 | self.vp8enc.set_property('min_quantizer', 1) | |
170 | self.vp8enc.set_property('max_quantizer', 13) | |
171 | self.vp8enc.set_property('cpu-used', 5) | |
172 | self.vp8enc.set_property('deadline', 42000) | |
173 | self.vp8enc.set_property('threads', 2) | |
174 | self.vp8enc.set_property('sharpness', 7) | |
175 | self.webmmux = Gst.ElementFactory.make('webmmux', 'webmmux') | |
176 | self.webmmux.set_property('streamable', True) | |
177 | ||
178 | self.tee_streamvideo = Gst.ElementFactory.make('tee', 'tee_streamvideo') | |
179 | self.tee_streamaudio = Gst.ElementFactory.make('tee', 'tee_streamaudio') | |
180 | self.queuea_1 = Gst.ElementFactory.make('queue', 'queuea_1') | |
181 | self.queuea_2 = Gst.ElementFactory.make('queue', 'queuea_2') | |
182 | ||
183 | self.disksink_audio = Gst.ElementFactory.make('filesink') | |
184 | self.disksink_audio.set_property('location', 'popo_audio') | |
185 | self.disksink_stream = Gst.ElementFactory.make('filesink') | |
186 | self.disksink_stream.set_property('location', 'popo_stream') | |
187 | ||
188 | self.icecastsink_audio = Gst.ElementFactory.make('shout2send', 'icecastsink_audio') | |
189 | # Configuration should be written on a file locally to keep safe private addresses | |
190 | self.icecastsink_audio.set_property('ip', 'live2.fsf.org') | |
191 | self.icecastsink_audio.set_property('port', 80) | |
192 | self.icecastsink_audio.set_property('mount', 'testaudio.ogv') | |
193 | self.icecastsink_audio.set_property('password', '') | |
194 | self.icecastsink_stream = Gst.ElementFactory.make('shout2send', 'icecastsink_stream') | |
195 | self.icecastsink_stream.set_property('ip', 'live2.fsf.org') | |
196 | self.icecastsink_stream.set_property('port', 80) | |
197 | self.icecastsink_stream.set_property('mount', 'teststream.ogv') | |
198 | self.icecastsink_stream.set_property('password', '') | |
199 | #--------------------------------------------------------------------------- | |
200 | ||
6fdd41d9 | 201 | # Add the elements to the pipeline. |
af8c02a4 | 202 | # Test of the first two lines of quidam's pipeline: |
6fdd41d9 | 203 | self.streampipe.add(self.videosrc) |
af8c02a4 DT |
204 | ## self.streampipe.add(self.decodebin) |
205 | self.streampipe.add(self.queuev_1) | |
206 | self.streampipe.add(self.rtpjpegdepay) | |
207 | self.streampipe.add(self.queuev_2) | |
208 | self.streampipe.add(self.jpegdec) | |
209 | self.streampipe.add(self.tee_rawvideo) | |
210 | self.streampipe.add(self.queuev_3) | |
211 | self.streampipe.add(self.mkvmux) | |
212 | self.streampipe.add(self.queuev_4) | |
213 | self.streampipe.add(self.disksink_rawvideo) | |
6fdd41d9 | 214 | self.streampipe.add(self.screensink) |
6fdd41d9 | 215 | |
af8c02a4 DT |
216 | |
217 | # Link the elements in the pipeline. | |
218 | ## self.videosrc.link(self.decodebin) | |
219 | self.queuev_1.link(self.rtpjpegdepay) | |
220 | ## self.rtpjpegdepay.link(self.queuev_2) | |
221 | ## self.rtpjpegdepay.link(self.jpegdec) | |
222 | self.rtpjpegdepay.link(self.tee_rawvideo) | |
223 | ## self.queuev_2.link(self.jpegdec) | |
224 | ## self.jpegdec.link(self.tee_rawvideo) | |
225 | ## self.jpegdec.link(self.queuev_3) | |
226 | self.tee_rawvideo.link(self.queuev_2) | |
227 | self.tee_rawvideo.link(self.jpegdec) | |
228 | ## self.tee_rawvideo.link(self.queuev_3) | |
229 | self.queuev_2.link(self.mkvmux) | |
230 | self.mkvmux.link(self.queuev_4) | |
231 | self.queuev_4.link(self.disksink_rawvideo) | |
232 | ## self.decodebin.link(self.screensink) | |
233 | ## self.queuev_3.link(self.disksink_rawvideo) | |
234 | self.jpegdec.link(self.queuev_3) | |
235 | self.queuev_3.link(self.screensink) | |
236 | ||
6fdd41d9 DT |
237 | return self.streampipe |
238 | ||
239 | def on_message(self, bus, message): | |
240 | ||
241 | t = message.type | |
242 | if t == Gst.MessageType.EOS: | |
af8c02a4 DT |
243 | self.pipel.set_state(Gst.State.NULL) |
244 | self.stream_button.set_label('Stream') | |
6fdd41d9 | 245 | elif t == Gst.MessageType.ERROR: |
6fdd41d9 DT |
246 | err, debug = message.parse_error() |
247 | print ("Error: %s" % err, debug) | |
af8c02a4 DT |
248 | self.pipel.set_state(Gst.State.NULL) |
249 | self.stream_button.set_label('Stream') | |
6fdd41d9 DT |
250 | |
251 | def on_sync_message(self, bus, message): | |
252 | ||
253 | if message.get_structure().get_name() == 'prepare-window-handle': | |
254 | imagesink = message.src | |
255 | imagesink.set_property('force-aspect-ratio', True) | |
256 | imagesink.set_window_handle(self.videowidget.get_property('window').get_xid()) | |
257 | ||
258 | ||
259 | # USE THAT FUNCTION TO GET THE SOURCE CHOICE (ELPHEL OR WEBCAM) | |
260 | def on_file_selected(self, widget): | |
261 | ||
262 | self.multimedia_file = self.load_file.get_filename() | |
263 | ||
264 | def on_stream_clicked(self, widget): | |
265 | ||
6fdd41d9 DT |
266 | labelname = self.stream_button.get_label() |
267 | if labelname == 'Stream': | |
268 | self.pipel.set_state(Gst.State.PLAYING) | |
269 | self.stream_button.set_label('ON AIR') | |
270 | elif labelname == 'ON AIR': | |
271 | self.pipel.set_state(Gst.State.NULL) | |
272 | self.stream_button.set_label('Stream') | |
273 | ||
274 | ||
275 | if __name__ == "__main__": | |
276 | Gst.init() | |
277 | Streamgui() | |
278 | Gtk.main() |