Skip to content

Commit fe20560

Browse files
author
Mariusz Klochowicz
committed
Add first cut of the application
1 parent e67753f commit fe20560

7 files changed

+1012
-0
lines changed

Diff for: CMakeLists.txt

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
cmake_minimum_required(VERSION 3.10)
2+
3+
project(ThreeVideoStream LANGUAGES C)
4+
5+
add_compile_options(-Wall -Wextra -pedantic -Werror -Wno-unused-parameter)
6+
7+
# for LSP / clang-tidy etc.
8+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
9+
10+
set (CMAKE_C_STANDARD 99)
11+
SET (CMAKE_C_FLAGS_DEBUG "-g -O0")
12+
13+
# for sanitizer build
14+
# set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0 -fno-omit-frame-pointer -fsanitize=address")
15+
# set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
16+
17+
# Glib
18+
find_package(PkgConfig REQUIRED)
19+
pkg_check_modules(GSTLIBS REQUIRED
20+
gobject-2.0
21+
glib-2.0
22+
gstreamer-1.0)
23+
24+
# add extra include directories
25+
include_directories(
26+
/usr/lib/x86_64-linux-gnu/glib-2.0/include
27+
/usr/include/glib-2.0
28+
/usr/include/gstreamer-1.0
29+
)
30+
31+
link_libraries(gstreamer-1.0
32+
gobject-2.0
33+
glib-2.0)
34+
35+
link_directories(${GSTLIBS_LIBRARY_DIRS})
36+
37+
set(SOURCE_FILES main.c three_video_stream.h three_video_stream.c gst_helpers.h gst_helpers.c)
38+
39+
add_executable(ThreeVideoStream ${SOURCE_FILES})
40+
41+
target_link_libraries(ThreeVideoStream ${ThreeVideoStream_LIBRARIES})

Diff for: README.md

+16
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,25 @@
44

55
Example C application utilising GStreamer to mix 3 videos onto one screen and optionally stream the result to Twitch via RTMP.
66

7+
Tested on Ubuntu 20.04.
8+
9+
# Features
10+
- Mixing 3 videos into one screen (which size can be configured)
11+
- optional Twitch streaming
12+
- core functionality is wrapped inside a GObject class, allowing for usage outside of C
13+
14+
# Usage
15+
`ThreeVideoStream` object encapsulates all the functionality and exposes a set of properties that can affect the playback.
16+
Optional properties should be set before the property `ready-to-play` is set.
17+
18+
Note: `ThreeVideoStream` exposes `GstPipeline *` as one of its properties to allow state monitoring & state changes of the underlying GStreamer pipeline.
19+
720
# Roadmap
821
- [ ] add audio support
22+
- [ ] implement more layouts (and expose a enum property to change them at runtime)
23+
- [ ] allow building on MacOS & Windows
924
- [ ] add GUI controls (GTK)
1025
- [ ] allow dynamically reconfiguring the pipeline
26+
- [ ] add support for streaming to other RMTP services
1127
- [ ] rewrite in Rust :)
1228

Diff for: gst_helpers.c

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
#include "gst_helpers.h"
2+
3+
void scale_input_videos(GstreamerData * data, int output_width, int output_height);
4+
void setup_video_mixer_pads(GstreamerData * data, int output_width, int output_height);
5+
6+
/* Manually clean unused Gst Elements if not streaming to Twitch */
7+
/* TODO Create them on-demand instead of eagerly*/
8+
void clean_unused_streaming_gst_elements(GstreamerData * data);
9+
10+
GstreamerData create_data()
11+
{
12+
GstreamerData data;
13+
/* Create the elements */
14+
data.decodebin1 = gst_element_factory_make("uridecodebin3", "decodebin1");
15+
data.decodebin2 = gst_element_factory_make("uridecodebin3", "decodebin2");
16+
data.decodebin3 = gst_element_factory_make("uridecodebin3", "decodebin3");
17+
data.videoscale1 = gst_element_factory_make("videoscale", "videobox1");
18+
data.videoscale2 = gst_element_factory_make("videoscale", "videobox2");
19+
data.videoscale3 = gst_element_factory_make("videoscale", "videobox3");
20+
data.video_scaled_caps1 = gst_element_factory_make("capsfilter", "video_scaled_capsfilter1");
21+
data.video_scaled_caps2 = gst_element_factory_make("capsfilter", "video_scaled_capsfilter2");
22+
data.video_scaled_caps3 = gst_element_factory_make("capsfilter", "video_scaled_capsfilter3");
23+
data.video_mixer = gst_element_factory_make("videomixer", "videomixer");
24+
25+
data.tee = gst_element_factory_make("tee", "tee");
26+
27+
/* Live preview */
28+
data.queue_preview = gst_element_factory_make("queue", "queue_preview");
29+
data.convert_preview = gst_element_factory_make("videoconvert", "convert_preview");
30+
data.sink_preview = gst_element_factory_make("autovideosink", "sink_preview");
31+
32+
/* Twitch streaming */
33+
data.queue_streaming = gst_element_factory_make("queue", "queue_streaming");
34+
data.video_encoder_streaming = gst_element_factory_make("x264enc", "encoder_streaming");
35+
data.queue_encoded = gst_element_factory_make("queue", "queue_encoded");
36+
data.muxer_streaming = gst_element_factory_make("flvmux", "muxer_streaming");
37+
data.queue_muxed = gst_element_factory_make("queue", "queue_muxed");
38+
data.sink_rtmp = gst_element_factory_make("rtmpsink", "sink_streaming");
39+
40+
data.pipeline = gst_pipeline_new("pipeline");
41+
42+
if (!data.pipeline || !data.decodebin1 || !data.decodebin2 || !data.decodebin3 //
43+
|| !data.videoscale1 || !data.videoscale2 || !data.videoscale3 || !data.video_scaled_caps1
44+
|| !data.video_scaled_caps2 || !data.video_scaled_caps3 || !data.video_mixer || !data.tee //
45+
|| !data.convert_preview || !data.queue_preview || !data.sink_preview || //
46+
!data.queue_streaming || !data.video_encoder_streaming || !data.queue_encoded || !data.muxer_streaming
47+
|| !data.queue_muxed || !data.sink_rtmp) {
48+
g_printerr("Not all elements could be created.\n");
49+
exit(1);
50+
}
51+
52+
return data;
53+
}
54+
55+
56+
void link_pipeline_elements(GstreamerData * data, gboolean with_twitch)
57+
{
58+
g_return_if_fail(data != NULL);
59+
60+
if (with_twitch == FALSE) { clean_unused_streaming_gst_elements(data); }
61+
62+
gboolean error = FALSE;
63+
64+
/* Add the common part of the pipeline */
65+
gst_bin_add_many(GST_BIN(data->pipeline),
66+
data->decodebin1,
67+
data->decodebin2,
68+
data->decodebin3,
69+
data->videoscale1,
70+
data->videoscale2,
71+
data->videoscale3,
72+
data->video_scaled_caps1,
73+
data->video_scaled_caps2,
74+
data->video_scaled_caps3,
75+
data->video_mixer,
76+
data->convert_preview,
77+
data->sink_preview,
78+
NULL);
79+
80+
if (!gst_element_link(data->videoscale1, data->video_scaled_caps1)
81+
|| !gst_element_link(data->videoscale2, data->video_scaled_caps2)
82+
|| !gst_element_link(data->videoscale3, data->video_scaled_caps3)) {
83+
error = TRUE;
84+
}
85+
86+
if (with_twitch) {
87+
gst_bin_add_many(GST_BIN(data->pipeline),
88+
data->tee,
89+
data->queue_preview,
90+
data->queue_streaming,
91+
data->video_encoder_streaming,
92+
data->queue_encoded,
93+
data->muxer_streaming,
94+
data->queue_muxed,
95+
data->sink_rtmp,
96+
NULL);
97+
98+
g_print("Linking GStreamer elements for live preview and Twitch streaming .\n");
99+
if (!gst_element_link_many(data->video_mixer, data->tee, NULL)
100+
|| !gst_element_link_many(data->tee,
101+
data->queue_streaming,
102+
data->video_encoder_streaming,
103+
data->queue_encoded,
104+
data->muxer_streaming,
105+
data->queue_muxed,
106+
data->sink_rtmp,
107+
NULL)
108+
|| !gst_element_link_many(data->tee,
109+
data->queue_preview,
110+
data->convert_preview,
111+
data->sink_preview,
112+
NULL)) {
113+
error = TRUE;
114+
}
115+
}
116+
else {
117+
g_print("Linking the elements without Twitch streaming part.\n");
118+
if (!gst_element_link_many(data->video_mixer, data->convert_preview, data->sink_preview, NULL)) {
119+
error = TRUE;
120+
}
121+
}
122+
if (error) {
123+
g_printerr("Elements could not be linked.\n");
124+
gst_object_unref(data->pipeline);
125+
exit(1);
126+
}
127+
}
128+
129+
void setup_video_placement(GstreamerData * data, int output_width, int output_height)
130+
{
131+
g_return_if_fail(data != NULL);
132+
133+
g_object_set(data->video_mixer, "background", 1, NULL); /* set black background beneath */
134+
135+
scale_input_videos(data, output_width, output_height);
136+
setup_video_mixer_pads(data, output_width, output_height);
137+
}
138+
139+
void setup_file_sources(GstreamerData * data, gchar * file_path1, gchar * file_path2, gchar * filepath3)
140+
{
141+
g_return_if_fail(data != NULL);
142+
g_return_if_fail(file_path1 != NULL || file_path2 != NULL || filepath3 != NULL);
143+
144+
gchar * uri1 = g_strjoin("", "file://", file_path1, NULL);
145+
gchar * uri2 = g_strjoin("", "file://", file_path2, NULL);
146+
gchar * uri3 = g_strjoin("", "file://", filepath3, NULL);
147+
g_object_set(data->decodebin1, "uri", uri1, NULL);
148+
g_object_set(data->decodebin2, "uri", uri2, NULL);
149+
g_object_set(data->decodebin3, "uri", uri3, NULL);
150+
g_free(uri1);
151+
g_free(uri2);
152+
g_free(uri3);
153+
}
154+
155+
void setup_twitch_streaming(GstreamerData * data, gchar * twitch_api_key, gchar * twitch_server)
156+
{
157+
g_return_if_fail(data != NULL);
158+
g_return_if_fail(twitch_api_key != NULL || twitch_server != NULL);
159+
160+
/* Set the parameters for the Twitch stream */
161+
g_object_set(data->video_encoder_streaming, "threads", 0, NULL);
162+
g_object_set(data->video_encoder_streaming, "bitrate", 400, NULL);
163+
g_object_set(data->video_encoder_streaming, "tune", 4, NULL);
164+
g_object_set(data->video_encoder_streaming, "key-int-max", 30, NULL);
165+
166+
g_object_set(data->muxer_streaming, "streamable", TRUE, NULL);
167+
168+
gchar * location = g_strjoin("", twitch_server, twitch_api_key, NULL);
169+
g_object_set(data->sink_rtmp, "location", location, NULL);
170+
g_free(location);
171+
}
172+
173+
void clean_unused_streaming_gst_elements(GstreamerData * data)
174+
{
175+
g_return_if_fail(data != NULL);
176+
gst_object_unref(data->tee);
177+
gst_object_unref(data->queue_preview);
178+
gst_object_unref(data->queue_streaming);
179+
gst_object_unref(data->video_encoder_streaming);
180+
gst_object_unref(data->queue_encoded);
181+
gst_object_unref(data->muxer_streaming);
182+
gst_object_unref(data->queue_muxed);
183+
gst_object_unref(data->sink_rtmp);
184+
}
185+
186+
void try_change_pipeline_state(GstElement * pipeline, GstState state)
187+
{
188+
GstStateChangeReturn ret = gst_element_set_state(pipeline, state);
189+
if (ret == GST_STATE_CHANGE_FAILURE) {
190+
g_printerr("Unable to set the pipeline to the paused state.\n");
191+
gst_object_unref(pipeline);
192+
exit(1);
193+
}
194+
}
195+
196+
/* private functions' definitions */
197+
198+
void scale_input_videos(GstreamerData * data, int output_width, int output_height)
199+
{
200+
GstCaps * videocaps_half = gst_caps_new_simple("video/x-raw",
201+
"format",
202+
G_TYPE_STRING,
203+
"I420",
204+
"framerate",
205+
GST_TYPE_FRACTION,
206+
25,
207+
1,
208+
"pixel-aspect-ratio",
209+
GST_TYPE_FRACTION,
210+
1,
211+
1,
212+
"width",
213+
G_TYPE_INT,
214+
output_width / 2,
215+
"height",
216+
G_TYPE_INT,
217+
output_height / 2,
218+
NULL);
219+
220+
221+
g_object_set(data->video_scaled_caps1, "caps", videocaps_half, NULL);
222+
g_object_set(data->video_scaled_caps2, "caps", videocaps_half, NULL);
223+
g_object_set(data->video_scaled_caps3, "caps", videocaps_half, NULL);
224+
gst_caps_unref(videocaps_half);
225+
}
226+
227+
void setup_video_mixer_pads(GstreamerData * data, int output_width, int output_height)
228+
{
229+
/* Manually link the videomixer, which has "Request" pads */
230+
GstPad *videomixer_pad1, *video1_pad = NULL;
231+
GstPad *videomixer_pad2, *video2_pad = NULL;
232+
GstPad *videomixer_pad3, *video3_pad = NULL;
233+
234+
videomixer_pad1 = gst_element_get_request_pad(data->video_mixer, "sink_%u");
235+
videomixer_pad2 = gst_element_get_request_pad(data->video_mixer, "sink_%u");
236+
videomixer_pad3 = gst_element_get_request_pad(data->video_mixer, "sink_%u");
237+
238+
video1_pad = gst_element_get_static_pad(data->video_scaled_caps1, "src");
239+
video2_pad = gst_element_get_static_pad(data->video_scaled_caps2, "src");
240+
video3_pad = gst_element_get_static_pad(data->video_scaled_caps3, "src");
241+
242+
if (gst_pad_link(video1_pad, videomixer_pad1) != GST_PAD_LINK_OK
243+
|| gst_pad_link(video2_pad, videomixer_pad2) != GST_PAD_LINK_OK
244+
|| gst_pad_link(video3_pad, videomixer_pad3) != GST_PAD_LINK_OK) {
245+
g_printerr("Videomixer could not be linked.\n");
246+
gst_object_unref(data->pipeline);
247+
exit(1);
248+
}
249+
250+
g_object_set(videomixer_pad1, "xpos", 0, NULL);
251+
g_object_set(videomixer_pad1, "ypos", output_height / 4, NULL);
252+
g_object_set(videomixer_pad2, "xpos", output_width / 2, NULL);
253+
g_object_set(videomixer_pad2, "ypos", 0, NULL);
254+
g_object_set(videomixer_pad3, "xpos", output_width / 2, NULL);
255+
g_object_set(videomixer_pad3, "ypos", output_height / 2, NULL);
256+
257+
gst_object_unref(videomixer_pad1);
258+
gst_object_unref(videomixer_pad2);
259+
gst_object_unref(videomixer_pad3);
260+
gst_object_unref(video1_pad);
261+
gst_object_unref(video2_pad);
262+
gst_object_unref(video3_pad);
263+
}

Diff for: gst_helpers.h

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#ifndef _GST_HELPERS__H_
2+
#define _GST_HELPERS__H_
3+
4+
#include <gst/gst.h>
5+
6+
/* Structure to contain all our information, so we can pass it to callbacks */
7+
typedef struct _GstreamerData {
8+
GstElement * pipeline;
9+
GstElement * decodebin1;
10+
GstElement * decodebin2;
11+
GstElement * decodebin3;
12+
GstElement * videoscale1;
13+
GstElement * videoscale2;
14+
GstElement * videoscale3;
15+
GstElement * video_scaled_caps1;
16+
GstElement * video_scaled_caps2;
17+
GstElement * video_scaled_caps3;
18+
GstElement * video_mixer;
19+
GstElement * convert_preview;
20+
GstElement * sink_preview;
21+
// XXX Do not call the following if Twitch is not setup
22+
GstElement * tee;
23+
GstElement * queue_preview;
24+
GstElement * queue_streaming;
25+
GstElement * video_encoder_streaming;
26+
GstElement * queue_encoded;
27+
GstElement * muxer_streaming;
28+
GstElement * queue_muxed;
29+
GstElement * sink_rtmp;
30+
} GstreamerData;
31+
32+
/* A factory function that creates all necessary GstElements, struct */
33+
/* Exits on error. */
34+
GstreamerData create_data();
35+
36+
void link_pipeline_elements(GstreamerData * data, gboolean with_twitch);
37+
38+
void setup_video_placement(GstreamerData * data, int output_width, int output_height);
39+
40+
void setup_file_sources(GstreamerData * data, gchar * filepath1, gchar * filepath2, gchar * filepath3);
41+
42+
void setup_twitch_streaming(GstreamerData * data, gchar * twitch_api_key, gchar * twich_server);
43+
44+
void clean_unused_streaming_gst_elements(GstreamerData * data);
45+
46+
/* Try to change pipeline state to desired state */
47+
/* Exits the program if request cannot be fulfilled */
48+
void try_change_pipeline_state(GstElement * pipeline, GstState state);
49+
50+
#endif /* _GST_HELPERS__H_ */

0 commit comments

Comments
 (0)