Last active
June 5, 2023 18:30
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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