Skip to content

Instantly share code, notes, and snippets.

@carrotIndustries
Last active June 5, 2023 18:30
Show Gist options
  • Save carrotIndustries/5cbebcb38650253e55c40d9195a7a98c to your computer and use it in GitHub Desktop.
Save carrotIndustries/5cbebcb38650253e55c40d9195a7a98c to your computer and use it in GitHub Desktop.
Really hacky application that renders shadertoy-compatible shaders and outputs an RTSP stream
//g++ appsrc.cpp $(pkg-config --libs --cflags gstreamer-1.0 osmesa gstreamer-rtsp-server-1.0) -o appsrc
// based on https://github.com/GStreamer/gst-rtsp-server/blob/master/examples/test-appsrc.c
// LICENSE: GPLv2
#include <gst/gst.h>
#include <gst/rtsp-server/rtsp-server.h>
#define GL_GLEXT_PROTOTYPES 1
//#include <GL/glext.h>
//#include <GL/gl.h>
//#include <GL/glcorearb.h>
#include <GL/osmesa.h>
#include <vector>
#include <cassert>
#include <stdexcept>
#include <iostream>
static GLuint create_shader_from_string(int type, const char *src)
{
auto shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
int status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
int log_len;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len);
std::string log_space(log_len + 1, ' ');
glGetShaderInfoLog(shader, log_len, nullptr, (GLchar *)log_space.c_str());
std::cerr << "Compile failure in ";
switch (type) {
case GL_VERTEX_SHADER:
std::cerr << "vertex";
break;
case GL_FRAGMENT_SHADER:
std::cerr << "fragment";
break;
case GL_GEOMETRY_SHADER:
std::cerr << "geometry";
break;
}
std::cerr << " shader: " << log_space << std::endl;
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint gl_create_program_from_string(const char *vertex_resource, const char *fragment_resource,
const char *geometry_resource)
{
GLuint vertex, fragment, geometry = 0;
GLuint program = 0;
int status;
vertex = create_shader_from_string(GL_VERTEX_SHADER, vertex_resource);
if (vertex == 0) {
return 0;
}
fragment = create_shader_from_string(GL_FRAGMENT_SHADER, fragment_resource);
if (fragment == 0) {
glDeleteShader(vertex);
return 0;
}
if (geometry_resource) {
geometry = create_shader_from_string(GL_GEOMETRY_SHADER, geometry_resource);
if (geometry == 0) {
glDeleteShader(vertex);
glDeleteShader(fragment);
}
}
program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
if (geometry) {
glAttachShader(program, geometry);
}
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
int log_len;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_len);
std::string log_space(log_len + 1, ' ');
glGetProgramInfoLog(program, log_len, nullptr, (GLchar *)log_space.c_str());
std::cerr << "Linking failure: " << log_space << std::endl;
glDeleteProgram(program);
program = 0;
goto out;
}
glDetachShader(program, vertex);
glDetachShader(program, fragment);
if (geometry)
glDetachShader(program, geometry);
out:
glDeleteShader(vertex);
glDeleteShader(fragment);
if (geometry)
glDeleteShader(geometry);
return program;
}
static GLuint create_vao(GLuint program)
{
GLuint position_index = glGetAttribLocation(program, "position");
GLuint vao, buffer;
/* we need to create a VAO to store the other buffers */
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
/* this is the VBO that holds the vertex data */
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
float vertices[] = {// Position
-1, 1, 1, 1, -1, -1, 1, -1};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
/* enable and set the position attribute */
glEnableVertexAttribArray(position_index);
glVertexAttribPointer(position_index, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
/* enable and set the color attribute */
/* reset the state; we will re-enable the VAO when needed */
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// glDeleteBuffers (1, &buffer);
return vao;
}
static const char *vertex_shader = R"END(
#version 330
in vec2 position;
out vec3 color_to_fragment;
out vec2 pos_to_fragment;
void main() {
if(gl_VertexID < 2) {
color_to_fragment = vec3(1,0,0);
}
else {
color_to_fragment = vec3(0,1,0);
}
gl_Position = vec4(position, -1, 1);
pos_to_fragment = position/2 + vec2(.5,.5);
}
)END";
static const char *fragment_shader = R"END(
#version 330
layout(location = 0) out vec4 outputColor;
in vec3 color_to_fragment;
in vec2 pos_to_fragment;
uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
/*
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
int x = int(uv.x*5.);
int y = int(uv.y*5.);
fragColor.xyz = hsv2rgb(vec3(sin(iTime/2.+float(x)*1.3+float(y))/2.+.5, 1, 1));
}
*/
float sstep(float x, float g) {
x -= .5;
x *= g;
x += .5;
return clamp(x, 0., 1.);
}
float wrap(float x) {
if(x < 0.5)
return x;
else
return 1.-x;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
float n = 5.;
float border_width = .025;
int x = int(uv.x*n);
int y = int(uv.y*n);
vec3 c = hsv2rgb(vec3(sin(iTime/2.+float(x)*1.5+float(y))/2.+.5, 1, 1));
float w = mod(iTime/2. + float(x)*1.5+pow(float(y),1.3), 1.3)/1.3;
//float w = sstep(w-.4, 10.);
w = sstep(w, 10.);
fragColor.xyz = c*w;
if((wrap(abs(uv.x*n - float(x))) < border_width) || (wrap(abs(uv.y*n - float(y))) < border_width))
{
fragColor.rgb = mix(vec3(1,1,1), c*w, clamp(sin(iTime*1.), .2, .8));
}
}
void main() {
outputColor = vec4(pos_to_fragment, 0, 1);
mainImage(outputColor, gl_FragCoord.xy);
}
)END";
typedef struct
{
gboolean white;
GstClockTime timestamp;
OSMesaContext mesa_ctx;
std::vector<unsigned char> gl_buffer;
GLuint program;
GLuint vao;
GLuint iResolution_loc;
GLuint iTime_loc;
} MyContext;
class Program {
public:
GLuint get_program() const {
return program;
}
private:
GLuint program;
GLuint vao;
GLuint iResolution_loc;
GLuint iTime_loc;
};
static const int vwidth = 720;
static const int vheight = 720;
static const int bytes_per_pixel = 4;
/* called when we need to give data to appsrc */
static void
need_data (GstElement * appsrc, guint unused, MyContext * ctx)
{
//std::cout << "A" << std::endl;
GstBuffer *buffer;
guint size;
GstFlowReturn ret;
size = vwidth * vheight * bytes_per_pixel;
buffer = gst_buffer_new_allocate (NULL, size, NULL);
/* this makes the image black/white */
//gst_buffer_memset (buffer, 0, ctx->white ? 0xff : 0x0, size);
if (!OSMesaMakeCurrent(ctx->mesa_ctx, ctx->gl_buffer.data(), GL_UNSIGNED_BYTE, vwidth, vheight)) {
throw std::runtime_error("couldn't make current");
}
glClearColor(1,0,ctx->white,1);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(ctx->program);
glBindVertexArray(ctx->vao);
glUniform3f(ctx->iResolution_loc, vwidth, vheight, 1);
glUniform1f(ctx->iTime_loc, ctx->timestamp/1e9);
// gl_color_to_uniform_3f(color_top_loc, ca.background_top_color);
// gl_color_to_uniform_3f(color_bottom_loc, ca.background_bottom_color);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glFlush();
glFinish();
GstMapInfo map_info = GST_MAP_INFO_INIT;
assert(gst_buffer_map(buffer, &map_info, GST_MAP_WRITE));
assert(map_info.size == size);
assert(ctx->gl_buffer.size() == size);
memcpy(map_info.data, ctx->gl_buffer.data(), ctx->gl_buffer.size());
gst_buffer_unmap(buffer, &map_info);
ctx->white = !ctx->white;
/* increment the timestamp every 1/2 second */
GST_BUFFER_PTS (buffer) = ctx->timestamp;
GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale_int (1, GST_SECOND, 30);
ctx->timestamp += GST_BUFFER_DURATION (buffer);
g_signal_emit_by_name (appsrc, "push-buffer", buffer, &ret);
gst_buffer_unref (buffer);
//std::cout << "B" << std::endl;
}
/* called when a new media pipeline is constructed. We can query the
* pipeline and configure our appsrc */
static void
media_configure (GstRTSPMediaFactory * factory, GstRTSPMedia * media,
gpointer user_data)
{
GstElement *element, *appsrc;
MyContext *ctx;
/* get the element used for providing the streams of the media */
element = gst_rtsp_media_get_element (media);
/* get our appsrc, we named it 'mysrc' with the name property */
appsrc = gst_bin_get_by_name_recurse_up (GST_BIN (element), "mysrc");
/* this instructs appsrc that we will be dealing with timed buffer */
gst_util_set_object_arg (G_OBJECT (appsrc), "format", "time");
/* configure the caps of the video */
g_object_set (G_OBJECT (appsrc), "caps",
gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, "RGBx",
"width", G_TYPE_INT, vwidth,
"height", G_TYPE_INT, vheight,
"framerate", GST_TYPE_FRACTION, 30, 1, NULL), NULL);
ctx = new MyContext;
ctx->white = FALSE;
ctx->timestamp = 0;
{
std::vector<int> attribs;
// attribs.push_back(OSMESA_DEPTH_BITS);
// attribs.push_back(16);
attribs.push_back(OSMESA_PROFILE);
attribs.push_back(OSMESA_CORE_PROFILE);
attribs.push_back(OSMESA_CONTEXT_MAJOR_VERSION);
attribs.push_back(3);
attribs.push_back(0);
attribs.push_back(0);
ctx->mesa_ctx = OSMesaCreateContextAttribs(attribs.data(), NULL);
}
ctx->gl_buffer.resize(vwidth * vheight * bytes_per_pixel);
if (!OSMesaMakeCurrent(ctx->mesa_ctx, ctx->gl_buffer.data(), GL_UNSIGNED_BYTE, vwidth, vheight)) {
throw std::runtime_error("couldn't make current");
}
ctx->program = gl_create_program_from_string(vertex_shader, fragment_shader, nullptr);
ctx->vao = create_vao(ctx->program);
ctx->iTime_loc = glGetUniformLocation(ctx->program, "iTime");
ctx->iResolution_loc = glGetUniformLocation(ctx->program, "iResolution");
glUniform3f(ctx->iResolution_loc, vwidth, vheight, 1);
glUniform1f(ctx->iTime_loc, 1);
glClearColor(1,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
glFlush();
/* make sure ther datais freed when the media is gone */
g_object_set_data_full (G_OBJECT (media), "my-extra-data", ctx,
(GDestroyNotify) g_free);
/* install the callback that will be called when a buffer is needed */
g_signal_connect (appsrc, "need-data", (GCallback) need_data, ctx);
gst_object_unref (appsrc);
gst_object_unref (element);
}
int
main (int argc, char *argv[])
{
GMainLoop *loop;
GstRTSPServer *server;
GstRTSPMountPoints *mounts;
GstRTSPMediaFactory *factory;
gst_init (&argc, &argv);
loop = g_main_loop_new (NULL, FALSE);
/* create a server instance */
server = gst_rtsp_server_new ();
/* get the mount points for this server, every server has a default object
* that be used to map uri mount points to media factories */
mounts = gst_rtsp_server_get_mount_points (server);
/* make a media factory for a test stream. The default media factory can use
* gst-launch syntax to create pipelines.
* any launch line works as long as it contains elements named pay%d. Each
* element with pay%d names will be a stream */
factory = gst_rtsp_media_factory_new ();
gst_rtsp_media_factory_set_launch (factory,
"( appsrc name=mysrc ! videoconvert ! x264enc ! rtph264pay name=pay0 pt=96 )");
/* notify when our media is ready, This is called whenever someone asks for
* the media and a new pipeline with our appsrc is created */
g_signal_connect (factory, "media-configure", (GCallback) media_configure,
NULL);
/* attach the test factory to the /test url */
gst_rtsp_mount_points_add_factory (mounts, "/test", factory);
/* don't need the ref to the mounts anymore */
g_object_unref (mounts);
/* attach the server to the default maincontext */
gst_rtsp_server_attach (server, NULL);
/* start serving */
g_print ("stream ready at rtsp://127.0.0.1:8554/test\n");
g_main_loop_run (loop);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment