mirror of
https://github.com/ossrs/srs.git
synced 2025-03-09 15:49:59 +00:00
listen and serve stream caster mpegts over udp.
This commit is contained in:
parent
f9d1e1111a
commit
52891b491a
8 changed files with 338 additions and 13 deletions
|
@ -323,6 +323,12 @@ else
|
||||||
echo "#undef SRS_AUTO_HTTP_SERVER" >> $SRS_AUTO_HEADERS_H
|
echo "#undef SRS_AUTO_HTTP_SERVER" >> $SRS_AUTO_HEADERS_H
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ $SRS_STREAM_CASTER = YES ]; then
|
||||||
|
echo "#define SRS_AUTO_STREAM_CASTER" >> $SRS_AUTO_HEADERS_H
|
||||||
|
else
|
||||||
|
echo "#undef SRS_AUTO_STREAM_CASTER" >> $SRS_AUTO_HEADERS_H
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $SRS_HTTP_API = YES ]; then
|
if [ $SRS_HTTP_API = YES ]; then
|
||||||
echo "#define SRS_AUTO_HTTP_API" >> $SRS_AUTO_HEADERS_H
|
echo "#define SRS_AUTO_HTTP_API" >> $SRS_AUTO_HEADERS_H
|
||||||
else
|
else
|
||||||
|
|
|
@ -25,6 +25,7 @@ SRS_INGEST=RESERVED
|
||||||
SRS_STAT=RESERVED
|
SRS_STAT=RESERVED
|
||||||
SRS_HTTP_CALLBACK=RESERVED
|
SRS_HTTP_CALLBACK=RESERVED
|
||||||
SRS_HTTP_SERVER=RESERVED
|
SRS_HTTP_SERVER=RESERVED
|
||||||
|
SRS_STREAM_CASTER=RESERVED
|
||||||
SRS_HTTP_API=RESERVED
|
SRS_HTTP_API=RESERVED
|
||||||
SRS_LIBRTMP=RESERVED
|
SRS_LIBRTMP=RESERVED
|
||||||
SRS_RESEARCH=RESERVED
|
SRS_RESEARCH=RESERVED
|
||||||
|
@ -114,6 +115,7 @@ Options:
|
||||||
build nginx at: ./objs/nginx/sbin/nginx
|
build nginx at: ./objs/nginx/sbin/nginx
|
||||||
--with-http-callback enable http hooks, build cherrypy as demo api server.
|
--with-http-callback enable http hooks, build cherrypy as demo api server.
|
||||||
--with-http-server enable http server to delivery http stream.
|
--with-http-server enable http server to delivery http stream.
|
||||||
|
--with-stream-caster enable stream caster to serve other stream over other protocol.
|
||||||
--with-http-api enable http api, to manage SRS by http api.
|
--with-http-api enable http api, to manage SRS by http api.
|
||||||
--with-ffmpeg enable transcoding tool ffmpeg.
|
--with-ffmpeg enable transcoding tool ffmpeg.
|
||||||
build ffmpeg at: ./objs/ffmpeg/bin/ffmpeg
|
build ffmpeg at: ./objs/ffmpeg/bin/ffmpeg
|
||||||
|
@ -138,6 +140,7 @@ Options:
|
||||||
--without-nginx disable delivery HTTP stream with nginx.
|
--without-nginx disable delivery HTTP stream with nginx.
|
||||||
--without-http-callback disable http, http hooks callback.
|
--without-http-callback disable http, http hooks callback.
|
||||||
--without-http-server disable http server, use external server to delivery http stream.
|
--without-http-server disable http server, use external server to delivery http stream.
|
||||||
|
--without-stream-caster disable stream caster, only listen and serve RTMP/HTTP.
|
||||||
--without-http-api disable http api, only use console to manage SRS process.
|
--without-http-api disable http api, only use console to manage SRS process.
|
||||||
--without-ffmpeg disable the ffmpeg transcode tool feature.
|
--without-ffmpeg disable the ffmpeg transcode tool feature.
|
||||||
--without-transcode disable the transcoding feature.
|
--without-transcode disable the transcoding feature.
|
||||||
|
@ -213,6 +216,7 @@ function parse_user_option() {
|
||||||
--with-stat) SRS_STAT=YES ;;
|
--with-stat) SRS_STAT=YES ;;
|
||||||
--with-http-callback) SRS_HTTP_CALLBACK=YES ;;
|
--with-http-callback) SRS_HTTP_CALLBACK=YES ;;
|
||||||
--with-http-server) SRS_HTTP_SERVER=YES ;;
|
--with-http-server) SRS_HTTP_SERVER=YES ;;
|
||||||
|
--with-stream-caster) SRS_STREAM_CASTER=YES ;;
|
||||||
--with-http-api) SRS_HTTP_API=YES ;;
|
--with-http-api) SRS_HTTP_API=YES ;;
|
||||||
--with-librtmp) SRS_LIBRTMP=YES ;;
|
--with-librtmp) SRS_LIBRTMP=YES ;;
|
||||||
--with-research) SRS_RESEARCH=YES ;;
|
--with-research) SRS_RESEARCH=YES ;;
|
||||||
|
@ -235,6 +239,7 @@ function parse_user_option() {
|
||||||
--without-stat) SRS_STAT=NO ;;
|
--without-stat) SRS_STAT=NO ;;
|
||||||
--without-http-callback) SRS_HTTP_CALLBACK=NO ;;
|
--without-http-callback) SRS_HTTP_CALLBACK=NO ;;
|
||||||
--without-http-server) SRS_HTTP_SERVER=NO ;;
|
--without-http-server) SRS_HTTP_SERVER=NO ;;
|
||||||
|
--without-stream-caster) SRS_STREAM_CASTER=NO ;;
|
||||||
--without-http-api) SRS_HTTP_API=NO ;;
|
--without-http-api) SRS_HTTP_API=NO ;;
|
||||||
--without-librtmp) SRS_LIBRTMP=NO ;;
|
--without-librtmp) SRS_LIBRTMP=NO ;;
|
||||||
--without-research) SRS_RESEARCH=NO ;;
|
--without-research) SRS_RESEARCH=NO ;;
|
||||||
|
@ -362,6 +367,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=NO
|
SRS_HTTP_PARSER=NO
|
||||||
SRS_HTTP_CALLBACK=NO
|
SRS_HTTP_CALLBACK=NO
|
||||||
SRS_HTTP_SERVER=NO
|
SRS_HTTP_SERVER=NO
|
||||||
|
SRS_STREAM_CASTER=NO
|
||||||
SRS_HTTP_API=NO
|
SRS_HTTP_API=NO
|
||||||
SRS_LIBRTMP=NO
|
SRS_LIBRTMP=NO
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -387,6 +393,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=YES
|
SRS_RESEARCH=YES
|
||||||
|
@ -412,6 +419,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=NO
|
SRS_HTTP_PARSER=NO
|
||||||
SRS_HTTP_CALLBACK=NO
|
SRS_HTTP_CALLBACK=NO
|
||||||
SRS_HTTP_SERVER=NO
|
SRS_HTTP_SERVER=NO
|
||||||
|
SRS_STREAM_CASTER=NO
|
||||||
SRS_HTTP_API=NO
|
SRS_HTTP_API=NO
|
||||||
SRS_LIBRTMP=NO
|
SRS_LIBRTMP=NO
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -437,6 +445,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=NO
|
SRS_HTTP_PARSER=NO
|
||||||
SRS_HTTP_CALLBACK=NO
|
SRS_HTTP_CALLBACK=NO
|
||||||
SRS_HTTP_SERVER=NO
|
SRS_HTTP_SERVER=NO
|
||||||
|
SRS_STREAM_CASTER=NO
|
||||||
SRS_HTTP_API=NO
|
SRS_HTTP_API=NO
|
||||||
SRS_LIBRTMP=NO
|
SRS_LIBRTMP=NO
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -462,6 +471,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=NO
|
SRS_HTTP_PARSER=NO
|
||||||
SRS_HTTP_CALLBACK=NO
|
SRS_HTTP_CALLBACK=NO
|
||||||
SRS_HTTP_SERVER=NO
|
SRS_HTTP_SERVER=NO
|
||||||
|
SRS_STREAM_CASTER=NO
|
||||||
SRS_HTTP_API=NO
|
SRS_HTTP_API=NO
|
||||||
SRS_LIBRTMP=NO
|
SRS_LIBRTMP=NO
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -487,6 +497,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -513,6 +524,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -538,6 +550,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -563,6 +576,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=YES
|
SRS_RESEARCH=YES
|
||||||
|
@ -588,6 +602,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=NO
|
SRS_LIBRTMP=NO
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -613,6 +628,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -638,6 +654,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -663,6 +680,7 @@ function apply_user_presets() {
|
||||||
SRS_HTTP_PARSER=YES
|
SRS_HTTP_PARSER=YES
|
||||||
SRS_HTTP_CALLBACK=YES
|
SRS_HTTP_CALLBACK=YES
|
||||||
SRS_HTTP_SERVER=YES
|
SRS_HTTP_SERVER=YES
|
||||||
|
SRS_STREAM_CASTER=YES
|
||||||
SRS_HTTP_API=YES
|
SRS_HTTP_API=YES
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=NO
|
SRS_RESEARCH=NO
|
||||||
|
@ -723,6 +741,7 @@ function apply_user_detail_options() {
|
||||||
SRS_HTTP_PARSER=NO
|
SRS_HTTP_PARSER=NO
|
||||||
SRS_HTTP_CALLBACK=NO
|
SRS_HTTP_CALLBACK=NO
|
||||||
SRS_HTTP_SERVER=NO
|
SRS_HTTP_SERVER=NO
|
||||||
|
SRS_STREAM_CASTER=NO
|
||||||
SRS_HTTP_API=NO
|
SRS_HTTP_API=NO
|
||||||
SRS_LIBRTMP=YES
|
SRS_LIBRTMP=YES
|
||||||
SRS_RESEARCH=YES
|
SRS_RESEARCH=YES
|
||||||
|
@ -752,6 +771,7 @@ function regenerate_options() {
|
||||||
if [ $SRS_STAT = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-stat"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-stat"; fi
|
if [ $SRS_STAT = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-stat"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-stat"; fi
|
||||||
if [ $SRS_HTTP_CALLBACK = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-callback"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-callback"; fi
|
if [ $SRS_HTTP_CALLBACK = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-callback"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-callback"; fi
|
||||||
if [ $SRS_HTTP_SERVER = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-server"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-server"; fi
|
if [ $SRS_HTTP_SERVER = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-server"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-server"; fi
|
||||||
|
if [ $SRS_STREAM_CASTER = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-stream-caster"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-stream-caster"; fi
|
||||||
if [ $SRS_HTTP_API = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-api"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-api"; fi
|
if [ $SRS_HTTP_API = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-http-api"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-http-api"; fi
|
||||||
if [ $SRS_LIBRTMP = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-librtmp"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-librtmp"; fi
|
if [ $SRS_LIBRTMP = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-librtmp"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-librtmp"; fi
|
||||||
if [ $SRS_RESEARCH = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-research"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-research"; fi
|
if [ $SRS_RESEARCH = YES ]; then SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --with-research"; else SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --without-research"; fi
|
||||||
|
@ -828,6 +848,7 @@ function check_option_conflicts() {
|
||||||
if [ $SRS_FFMPEG_TOOL = RESERVED ]; then echo "you must specifies the ffmpeg, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_FFMPEG_TOOL = RESERVED ]; then echo "you must specifies the ffmpeg, see: ./configure --help"; __check_ok=NO; fi
|
||||||
if [ $SRS_HTTP_CALLBACK = RESERVED ]; then echo "you must specifies the http-callback, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_HTTP_CALLBACK = RESERVED ]; then echo "you must specifies the http-callback, see: ./configure --help"; __check_ok=NO; fi
|
||||||
if [ $SRS_HTTP_SERVER = RESERVED ]; then echo "you must specifies the http-server, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_HTTP_SERVER = RESERVED ]; then echo "you must specifies the http-server, see: ./configure --help"; __check_ok=NO; fi
|
||||||
|
if [ $SRS_STREAM_CASTER = RESERVED ]; then echo "you must specifies the stream-caster, see: ./configure --help"; __check_ok=NO; fi
|
||||||
if [ $SRS_HTTP_API = RESERVED ]; then echo "you must specifies the http-api, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_HTTP_API = RESERVED ]; then echo "you must specifies the http-api, see: ./configure --help"; __check_ok=NO; fi
|
||||||
if [ $SRS_LIBRTMP = RESERVED ]; then echo "you must specifies the librtmp, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_LIBRTMP = RESERVED ]; then echo "you must specifies the librtmp, see: ./configure --help"; __check_ok=NO; fi
|
||||||
if [ $SRS_RESEARCH = RESERVED ]; then echo "you must specifies the research, see: ./configure --help"; __check_ok=NO; fi
|
if [ $SRS_RESEARCH = RESERVED ]; then echo "you must specifies the research, see: ./configure --help"; __check_ok=NO; fi
|
||||||
|
|
|
@ -142,6 +142,16 @@ stream_caster {
|
||||||
# whether stream caster is enabled.
|
# whether stream caster is enabled.
|
||||||
# default: off
|
# default: off
|
||||||
enabled on;
|
enabled on;
|
||||||
|
# the caster type of stream, the casters:
|
||||||
|
# mpegts_over_udp, MPEG-TS over UDP caster.
|
||||||
|
caster mpegts_over_udp;
|
||||||
|
# the output rtmp url.
|
||||||
|
# for example, rtmp://127.0.0.1/live/livestream.
|
||||||
|
output rtmp://127.0.0.1/live/livestream;
|
||||||
|
# the listen port for stream caster.
|
||||||
|
# for caster:
|
||||||
|
# mpegts_over_udp, listen at udp port.
|
||||||
|
listen 1935;
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################################################################################
|
#############################################################################################
|
||||||
|
|
|
@ -140,6 +140,11 @@ bool SrsConfDirective::is_vhost()
|
||||||
return name == "vhost";
|
return name == "vhost";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SrsConfDirective::is_stream_caster()
|
||||||
|
{
|
||||||
|
return name == "stream_caster";
|
||||||
|
}
|
||||||
|
|
||||||
int SrsConfDirective::parse(SrsConfigBuffer* buffer)
|
int SrsConfDirective::parse(SrsConfigBuffer* buffer)
|
||||||
{
|
{
|
||||||
return parse_conf(buffer, parse_file);
|
return parse_conf(buffer, parse_file);
|
||||||
|
@ -519,6 +524,8 @@ int SrsConfig::reload_conf(SrsConfig* conf)
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: FIXME: support reload stream_caster.
|
||||||
|
|
||||||
// merge config: vhost
|
// merge config: vhost
|
||||||
if ((ret = reload_vhost(old_root)) != ERROR_SUCCESS) {
|
if ((ret = reload_vhost(old_root)) != ERROR_SUCCESS) {
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -1295,6 +1302,7 @@ int SrsConfig::check_config()
|
||||||
srs_trace("srs checking config...");
|
srs_trace("srs checking config...");
|
||||||
|
|
||||||
vector<SrsConfDirective*> vhosts = get_vhosts();
|
vector<SrsConfDirective*> vhosts = get_vhosts();
|
||||||
|
vector<SrsConfDirective*> stream_casters = get_stream_casters();
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// check empty
|
// check empty
|
||||||
|
@ -1315,7 +1323,7 @@ int SrsConfig::check_config()
|
||||||
&& n != "srs_log_tank" && n != "srs_log_level" && n != "srs_log_file"
|
&& n != "srs_log_tank" && n != "srs_log_level" && n != "srs_log_file"
|
||||||
&& n != "max_connections" && n != "daemon" && n != "heartbeat"
|
&& n != "max_connections" && n != "daemon" && n != "heartbeat"
|
||||||
&& n != "http_api" && n != "stats" && n != "vhost" && n != "pithy_print"
|
&& n != "http_api" && n != "stats" && n != "vhost" && n != "pithy_print"
|
||||||
&& n != "http_stream" && n != "http_server")
|
&& n != "http_stream" && n != "http_server" && n != "stream_caster")
|
||||||
{
|
{
|
||||||
ret = ERROR_SYSTEM_CONFIG_INVALID;
|
ret = ERROR_SYSTEM_CONFIG_INVALID;
|
||||||
srs_error("unsupported directive %s, ret=%d", n.c_str(), ret);
|
srs_error("unsupported directive %s, ret=%d", n.c_str(), ret);
|
||||||
|
@ -1381,6 +1389,20 @@ int SrsConfig::check_config()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (int n = 0; n < (int)stream_casters.size(); n++) {
|
||||||
|
SrsConfDirective* stream_caster = stream_casters[n];
|
||||||
|
for (int i = 0; stream_caster && i < (int)stream_caster->directives.size(); i++) {
|
||||||
|
SrsConfDirective* conf = stream_caster->at(i);
|
||||||
|
string n = conf->name;
|
||||||
|
if (n != "enabled" && n != "caster" && n != "output"
|
||||||
|
&& n != "listen"
|
||||||
|
) {
|
||||||
|
ret = ERROR_SYSTEM_CONFIG_INVALID;
|
||||||
|
srs_error("unsupported stream_caster directive %s, ret=%d", n.c_str(), ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (int n = 0; n < (int)vhosts.size(); n++) {
|
for (int n = 0; n < (int)vhosts.size(); n++) {
|
||||||
SrsConfDirective* vhost = vhosts[n];
|
SrsConfDirective* vhost = vhosts[n];
|
||||||
for (int i = 0; vhost && i < (int)vhost->directives.size(); i++) {
|
for (int i = 0; vhost && i < (int)vhost->directives.size(); i++) {
|
||||||
|
@ -1969,6 +1991,77 @@ int SrsConfig::get_pithy_print_edge()
|
||||||
return ::atoi(pithy->arg0().c_str());
|
return ::atoi(pithy->arg0().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<SrsConfDirective*> SrsConfig::get_stream_casters()
|
||||||
|
{
|
||||||
|
srs_assert(root);
|
||||||
|
|
||||||
|
std::vector<SrsConfDirective*> stream_casters;
|
||||||
|
|
||||||
|
for (int i = 0; i < (int)root->directives.size(); i++) {
|
||||||
|
SrsConfDirective* conf = root->at(i);
|
||||||
|
|
||||||
|
if (!conf->is_stream_caster()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_casters.push_back(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream_casters;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SrsConfig::get_stream_caster_enabled(SrsConfDirective* sc)
|
||||||
|
{
|
||||||
|
srs_assert(sc);
|
||||||
|
|
||||||
|
SrsConfDirective* conf = sc->get("enabled");
|
||||||
|
if (!conf) {
|
||||||
|
return SRS_CONF_DEFAULT_STREAM_CASTER_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf->arg0() != "on") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string SrsConfig::get_stream_caster_engine(SrsConfDirective* sc)
|
||||||
|
{
|
||||||
|
srs_assert(sc);
|
||||||
|
|
||||||
|
SrsConfDirective* conf = sc->get("caster");
|
||||||
|
if (!conf) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf->arg0();
|
||||||
|
}
|
||||||
|
|
||||||
|
string SrsConfig::get_stream_caster_output(SrsConfDirective* sc)
|
||||||
|
{
|
||||||
|
srs_assert(sc);
|
||||||
|
|
||||||
|
SrsConfDirective* conf = sc->get("output");
|
||||||
|
if (!conf) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf->arg0();
|
||||||
|
}
|
||||||
|
|
||||||
|
int SrsConfig::get_stream_caster_listen(SrsConfDirective* sc)
|
||||||
|
{
|
||||||
|
srs_assert(sc);
|
||||||
|
|
||||||
|
SrsConfDirective* conf = sc->get("listen");
|
||||||
|
if (!conf) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ::atoi(conf->arg0().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
SrsConfDirective* SrsConfig::get_vhost(string vhost)
|
SrsConfDirective* SrsConfig::get_vhost(string vhost)
|
||||||
{
|
{
|
||||||
srs_assert(root);
|
srs_assert(root);
|
||||||
|
|
|
@ -80,6 +80,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
#define SRS_CONF_DEFAULT_SECURITY_ENABLED false
|
#define SRS_CONF_DEFAULT_SECURITY_ENABLED false
|
||||||
|
|
||||||
|
#define SRS_CONF_DEFAULT_STREAM_CASTER_ENABLED false
|
||||||
|
#define SRS_CONF_DEFAULT_STREAM_CASTER_MPEGTS_OVER_UDP "mpegts_over_udp"
|
||||||
|
|
||||||
#define SRS_CONF_DEFAULT_STATS_NETWORK_DEVICE_INDEX 0
|
#define SRS_CONF_DEFAULT_STATS_NETWORK_DEVICE_INDEX 0
|
||||||
|
|
||||||
#define SRS_CONF_DEFAULT_STAGE_PLAY_USER_INTERVAL_MS 10000
|
#define SRS_CONF_DEFAULT_STAGE_PLAY_USER_INTERVAL_MS 10000
|
||||||
|
@ -188,6 +191,10 @@ public:
|
||||||
* whether current directive is vhost.
|
* whether current directive is vhost.
|
||||||
*/
|
*/
|
||||||
virtual bool is_vhost();
|
virtual bool is_vhost();
|
||||||
|
/**
|
||||||
|
* whether current directive is stream_caster.
|
||||||
|
*/
|
||||||
|
virtual bool is_stream_caster();
|
||||||
// parse utilities
|
// parse utilities
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
@ -446,6 +453,28 @@ public:
|
||||||
* the edge will get stream from upnode.
|
* the edge will get stream from upnode.
|
||||||
*/
|
*/
|
||||||
virtual int get_pithy_print_edge();
|
virtual int get_pithy_print_edge();
|
||||||
|
// stream_caster section
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* get all stream_caster in config file.
|
||||||
|
*/
|
||||||
|
virtual std::vector<SrsConfDirective*> get_stream_casters();
|
||||||
|
/**
|
||||||
|
* get whether the specified stream_caster is enabled.
|
||||||
|
*/
|
||||||
|
virtual bool get_stream_caster_enabled(SrsConfDirective* sc);
|
||||||
|
/**
|
||||||
|
* get the engine of stream_caster, the caster config.
|
||||||
|
*/
|
||||||
|
virtual std::string get_stream_caster_engine(SrsConfDirective* sc);
|
||||||
|
/**
|
||||||
|
* get the output rtmp url of stream_caster, the output config.
|
||||||
|
*/
|
||||||
|
virtual std::string get_stream_caster_output(SrsConfDirective* sc);
|
||||||
|
/**
|
||||||
|
* get the listen port of stream caster.
|
||||||
|
*/
|
||||||
|
virtual int get_stream_caster_listen(SrsConfDirective* sc);
|
||||||
// vhost specified section
|
// vhost specified section
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -93,6 +93,22 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
// SRS_SYS_CYCLE_INTERVAL * SRS_SYS_NETWORK_DEVICE_RESOLUTION_TIMES
|
// SRS_SYS_CYCLE_INTERVAL * SRS_SYS_NETWORK_DEVICE_RESOLUTION_TIMES
|
||||||
#define SRS_SYS_NETWORK_DEVICE_RESOLUTION_TIMES 9
|
#define SRS_SYS_NETWORK_DEVICE_RESOLUTION_TIMES 9
|
||||||
|
|
||||||
|
std::string __srs_listener_type2string(SrsListenerType type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case SrsListenerRtmpStream:
|
||||||
|
return "RTMP";
|
||||||
|
case SrsListenerHttpApi:
|
||||||
|
return "HTTP-API";
|
||||||
|
case SrsListenerHttpStream:
|
||||||
|
return "HTTP-Server";
|
||||||
|
case SrsListenerMpegTsOverUdp:
|
||||||
|
return "MPEG-TS over UDP";
|
||||||
|
default:
|
||||||
|
return "UNKONWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SrsListener::SrsListener(SrsServer* server, SrsListenerType type)
|
SrsListener::SrsListener(SrsServer* server, SrsListenerType type)
|
||||||
{
|
{
|
||||||
fd = -1;
|
fd = -1;
|
||||||
|
@ -174,16 +190,13 @@ int SrsListener::listen(int port)
|
||||||
}
|
}
|
||||||
srs_verbose("create st listen thread success, port=%d", port);
|
srs_verbose("create st listen thread success, port=%d", port);
|
||||||
|
|
||||||
srs_trace("listen thread cid=%d, current_cid=%d, "
|
srs_info("listen thread cid=%d, current_cid=%d, "
|
||||||
"listen at port=%d, type=%d, fd=%d started success, port=%d",
|
"listen at port=%d, type=%d, fd=%d started success, port=%d",
|
||||||
pthread->cid(), _srs_context->get_id(), _port, _type, fd, port);
|
pthread->cid(), _srs_context->get_id(), _port, _type, fd, port);
|
||||||
|
|
||||||
return ret;
|
srs_trace("%s listen at tcp://%d, fd=%d", __srs_listener_type2string(_type).c_str(), _port, fd);
|
||||||
}
|
|
||||||
|
|
||||||
void SrsListener::on_thread_start()
|
return ret;
|
||||||
{
|
|
||||||
srs_trace("listen cycle start, port=%d, type=%d, fd=%d", _port, _type, fd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int SrsListener::cycle()
|
int SrsListener::cycle()
|
||||||
|
@ -207,6 +220,86 @@ int SrsListener::cycle()
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SrsUdpListener::SrsUdpListener(SrsServer* server, SrsListenerType type) : SrsListener(server, type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsUdpListener::~SrsUdpListener()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int SrsUdpListener::listen(int port)
|
||||||
|
{
|
||||||
|
int ret = ERROR_SUCCESS;
|
||||||
|
|
||||||
|
// the caller already ensure the type is ok,
|
||||||
|
// we just assert here for unknown stream caster.
|
||||||
|
srs_assert(_type == SrsListenerMpegTsOverUdp);
|
||||||
|
|
||||||
|
_port = port;
|
||||||
|
|
||||||
|
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
|
||||||
|
ret = ERROR_SOCKET_CREATE;
|
||||||
|
srs_error("create linux socket error. port=%d, ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_verbose("create linux socket success. port=%d, fd=%d", port, fd);
|
||||||
|
|
||||||
|
int reuse_socket = 1;
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse_socket, sizeof(int)) == -1) {
|
||||||
|
ret = ERROR_SOCKET_SETREUSE;
|
||||||
|
srs_error("setsockopt reuse-addr error. port=%d, ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_verbose("setsockopt reuse-addr success. port=%d, fd=%d", port, fd);
|
||||||
|
|
||||||
|
sockaddr_in addr;
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(_port);
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY;
|
||||||
|
if (bind(fd, (const sockaddr*)&addr, sizeof(sockaddr_in)) == -1) {
|
||||||
|
ret = ERROR_SOCKET_BIND;
|
||||||
|
srs_error("bind socket error. port=%d, ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_verbose("bind socket success. port=%d, fd=%d", port, fd);
|
||||||
|
|
||||||
|
if ((stfd = st_netfd_open_socket(fd)) == NULL){
|
||||||
|
ret = ERROR_ST_OPEN_SOCKET;
|
||||||
|
srs_error("st_netfd_open_socket open socket failed. port=%d, ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_verbose("st open socket success. port=%d, fd=%d", port, fd);
|
||||||
|
|
||||||
|
if ((ret = pthread->start()) != ERROR_SUCCESS) {
|
||||||
|
srs_error("st_thread_create listen thread error. port=%d, ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_verbose("create st listen thread success, port=%d", port);
|
||||||
|
|
||||||
|
srs_info("listen thread cid=%d, current_cid=%d, "
|
||||||
|
"listen at port=%d, type=%d, fd=%d started success, port=%d",
|
||||||
|
pthread->cid(), _srs_context->get_id(), _port, _type, fd, port);
|
||||||
|
|
||||||
|
srs_trace("%s listen at udp://%d, fd=%d", __srs_listener_type2string(_type).c_str(), _port, fd);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SrsUdpListener::cycle()
|
||||||
|
{
|
||||||
|
int ret = ERROR_SUCCESS;
|
||||||
|
|
||||||
|
// the caller already ensure the type is ok,
|
||||||
|
// we just assert here for unknown stream caster.
|
||||||
|
srs_assert(_type == SrsListenerMpegTsOverUdp);
|
||||||
|
|
||||||
|
// TODO: FIXME: recv udp packet.
|
||||||
|
st_sleep(1);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
SrsSignalManager* SrsSignalManager::instance = NULL;
|
SrsSignalManager* SrsSignalManager::instance = NULL;
|
||||||
|
|
||||||
SrsSignalManager::SrsSignalManager(SrsServer* server)
|
SrsSignalManager::SrsSignalManager(SrsServer* server)
|
||||||
|
@ -608,6 +701,10 @@ int SrsServer::listen()
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((ret = listen_stream_caster()) != ERROR_SUCCESS) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -869,6 +966,53 @@ int SrsServer::listen_http_stream()
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SrsServer::listen_stream_caster()
|
||||||
|
{
|
||||||
|
int ret = ERROR_SUCCESS;
|
||||||
|
|
||||||
|
#ifdef SRS_AUTO_STREAM_CASTER
|
||||||
|
close_listeners(SrsListenerMpegTsOverUdp);
|
||||||
|
|
||||||
|
std::vector<SrsConfDirective*>::iterator it;
|
||||||
|
std::vector<SrsConfDirective*> stream_casters = _srs_config->get_stream_casters();
|
||||||
|
|
||||||
|
for (it = stream_casters.begin(); it != stream_casters.end(); ++it) {
|
||||||
|
SrsConfDirective* stream_caster = *it;
|
||||||
|
if (!_srs_config->get_stream_caster_enabled(stream_caster)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SrsUdpListener* listener = NULL;
|
||||||
|
|
||||||
|
std::string caster = _srs_config->get_stream_caster_engine(stream_caster);
|
||||||
|
if (caster == SRS_CONF_DEFAULT_STREAM_CASTER_MPEGTS_OVER_UDP) {
|
||||||
|
listener = new SrsUdpListener(this, SrsListenerMpegTsOverUdp);
|
||||||
|
} else {
|
||||||
|
ret = ERROR_STREAM_CASTER_ENGINE;
|
||||||
|
srs_error("unsupported stream caster %s. ret=%d", caster.c_str(), ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
srs_assert(listener != NULL);
|
||||||
|
|
||||||
|
listeners.push_back(listener);
|
||||||
|
|
||||||
|
int port = _srs_config->get_stream_caster_listen(stream_caster);
|
||||||
|
if (port <= 0) {
|
||||||
|
ret = ERROR_STREAM_CASTER_PORT;
|
||||||
|
srs_error("invalid stream caster port %d. ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = listener->listen(port)) != ERROR_SUCCESS) {
|
||||||
|
srs_error("StreamCaster listen at port %d failed. ret=%d", port, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
void SrsServer::close_listeners(SrsListenerType type)
|
void SrsServer::close_listeners(SrsListenerType type)
|
||||||
{
|
{
|
||||||
std::vector<SrsListener*>::iterator it;
|
std::vector<SrsListener*>::iterator it;
|
||||||
|
|
|
@ -54,14 +54,19 @@ enum SrsListenerType
|
||||||
// HTTP api,
|
// HTTP api,
|
||||||
SrsListenerHttpApi = 1,
|
SrsListenerHttpApi = 1,
|
||||||
// HTTP stream, HDS/HLS/DASH
|
// HTTP stream, HDS/HLS/DASH
|
||||||
SrsListenerHttpStream = 2
|
SrsListenerHttpStream = 2,
|
||||||
|
// UDP stream, MPEG-TS over udp.
|
||||||
|
SrsListenerMpegTsOverUdp = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the common tcp listener, for RTMP/HTTP server.
|
||||||
|
*/
|
||||||
class SrsListener : public ISrsThreadHandler
|
class SrsListener : public ISrsThreadHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SrsListenerType _type;
|
SrsListenerType _type;
|
||||||
private:
|
protected:
|
||||||
int fd;
|
int fd;
|
||||||
st_netfd_t stfd;
|
st_netfd_t stfd;
|
||||||
int _port;
|
int _port;
|
||||||
|
@ -75,7 +80,21 @@ public:
|
||||||
virtual int listen(int port);
|
virtual int listen(int port);
|
||||||
// interface ISrsThreadHandler.
|
// interface ISrsThreadHandler.
|
||||||
public:
|
public:
|
||||||
virtual void on_thread_start();
|
virtual int cycle();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the udp listener, for udp server.
|
||||||
|
*/
|
||||||
|
class SrsUdpListener : public SrsListener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SrsUdpListener(SrsServer* server, SrsListenerType type);
|
||||||
|
virtual ~SrsUdpListener();
|
||||||
|
public:
|
||||||
|
virtual int listen(int port);
|
||||||
|
// interface ISrsThreadHandler.
|
||||||
|
public:
|
||||||
virtual int cycle();
|
virtual int cycle();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -211,6 +230,7 @@ private:
|
||||||
virtual int listen_rtmp();
|
virtual int listen_rtmp();
|
||||||
virtual int listen_http_api();
|
virtual int listen_http_api();
|
||||||
virtual int listen_http_stream();
|
virtual int listen_http_stream();
|
||||||
|
virtual int listen_stream_caster();
|
||||||
/**
|
/**
|
||||||
* close the listeners for specified type,
|
* close the listeners for specified type,
|
||||||
* remove the listen object from manager.
|
* remove the listen object from manager.
|
||||||
|
|
|
@ -204,7 +204,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#define ERROR_AAC_DATA_INVALID 3048
|
#define ERROR_AAC_DATA_INVALID 3048
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
// HTTP protocol error.
|
// HTTP/StreamCaster protocol error.
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
#define ERROR_HTTP_PATTERN_EMPTY 4000
|
#define ERROR_HTTP_PATTERN_EMPTY 4000
|
||||||
#define ERROR_HTTP_PATTERN_DUPLICATED 4001
|
#define ERROR_HTTP_PATTERN_DUPLICATED 4001
|
||||||
|
@ -216,6 +216,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
#define ERROR_AAC_DECODE_ERROR 4007
|
#define ERROR_AAC_DECODE_ERROR 4007
|
||||||
#define ERROR_KERNEL_MP3_STREAM_CLOSED 4008
|
#define ERROR_KERNEL_MP3_STREAM_CLOSED 4008
|
||||||
#define ERROR_MP3_DECODE_ERROR 4009
|
#define ERROR_MP3_DECODE_ERROR 4009
|
||||||
|
#define ERROR_STREAM_CASTER_ENGINE 4010
|
||||||
|
#define ERROR_STREAM_CASTER_PORT 4011
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* whether the error code is an system control error.
|
* whether the error code is an system control error.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue