├── etc ├── cron.d │ └── clean-hls-dir ├── supervisor │ └── conf.d │ │ └── supervisord.conf └── nginx │ └── nginx.conf ├── .dockerignore ├── api ├── go.mod ├── main.go ├── api │ └── api.go └── go.sum ├── docker-compose.yml ├── clean-hls-dir.sh ├── LICENSE ├── html ├── cover.css └── index.html ├── .gitignore ├── README.md └── Dockerfile /etc/cron.d/clean-hls-dir: -------------------------------------------------------------------------------- 1 | */10 * * * * /clean-hls-dir.sh > /dev/stdout -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | .idea 4 | README.md 5 | docker-compose.yml -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thiago-dev/nginx-rtmp-go/api 2 | 3 | require github.com/gin-gonic/gin v1.3.0 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: thiagodev/openresty-rtmp-ffmpeg-api:latest 5 | environment: 6 | - STREAM_KEY=bla 7 | ports: 8 | - "80:80" 9 | - "1935:1935" 10 | volumes: 11 | - ./etc/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro -------------------------------------------------------------------------------- /clean-hls-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # I experienced an error where .ts and .m3u8 files were being deleted prematurely. 4 | # Resulting in a playback error. 5 | # Fix: 6 | # set hls_cleanup to off in nginx.conf 7 | # To keep the directory clean use this script which will be called every 2 minutes 8 | # and delete every content of hls directory. 9 | echo "cleaning hls directory..!" 10 | rm -r $HLS_DIR/* -------------------------------------------------------------------------------- /etc/supervisor/conf.d/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | logfile=/dev/stdout 4 | logfile_maxbytes=0 5 | 6 | [program:go-rtmp-api] 7 | command=/go-rtmp-api 8 | autostart=true 9 | autorestart=true 10 | startsecs=5 11 | stdout_logfile=NONE 12 | stderr_logfile=NONE 13 | 14 | [program:openresty] 15 | command=/usr/local/openresty/bin/openresty "-g daemon off;" 16 | autostart=true 17 | autorestart=true 18 | startsecs=5 19 | stdout_logfile=NONE 20 | stderr_logfile=NONE 21 | 22 | [program:crond] 23 | command = /usr/sbin/crond 24 | user = root 25 | autostart = true 26 | stdout_logfile=NONE 27 | stderr_logfile=NONE -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 thiago-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/thiago-dev/nginx-rtmp-go/api/api" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "time" 12 | ) 13 | 14 | var listenAddr string 15 | 16 | func init() { 17 | listenAddr = os.Getenv("LISTEN_ADDR") 18 | if listenAddr == "" { 19 | listenAddr = ":3000" 20 | } 21 | // Disable console color 22 | gin.DisableConsoleColor() 23 | // enable release mode 24 | gin.SetMode(gin.ReleaseMode) 25 | } 26 | 27 | func main() { 28 | router := gin.Default() 29 | router.POST("/on_publish", api.OnPublish) 30 | 31 | // inform user if no api key were set 32 | if os.Getenv("STREAM_KEY") == "" { 33 | log.Println("no stream key set. check will always return 200.") 34 | } 35 | 36 | srv := &http.Server{ 37 | Addr: listenAddr, 38 | // Good practice to set timeouts to avoid Slowloris attacks. 39 | WriteTimeout: time.Second * 15, 40 | ReadTimeout: time.Second * 15, 41 | IdleTimeout: time.Second * 60, 42 | Handler: router, 43 | } 44 | 45 | // Run our server in a goroutine so that it doesn't block. 46 | go func() { 47 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 48 | log.Fatalf("listen: %s\n", err) 49 | } 50 | }() 51 | // Wait for interrupt signal to gracefully shutdown the server with 52 | // a timeout of 5 seconds. 53 | quit := make(chan os.Signal) 54 | signal.Notify(quit, os.Interrupt) 55 | <-quit 56 | 57 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 58 | defer cancel() 59 | if err := srv.Shutdown(ctx); err != nil { 60 | log.Fatal("Server Shutdown:", err) 61 | } 62 | log.Println("Exiting..") 63 | } 64 | 65 | -------------------------------------------------------------------------------- /html/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Source: https://getbootstrap.com/docs/4.0/examples/cover/cover.css 3 | */ 4 | /* 5 | * Globals 6 | */ 7 | 8 | /* Links */ 9 | a, 10 | a:focus, 11 | a:hover { 12 | color: #fff; 13 | } 14 | .icon-github:before { 15 | content: "\e900"; 16 | } 17 | 18 | /* Custom default button */ 19 | .btn-secondary, 20 | .btn-secondary:hover, 21 | .btn-secondary:focus { 22 | color: #333; 23 | text-shadow: none; /* Prevent inheritance from `body` */ 24 | background-color: #fff; 25 | border: .05rem solid #fff; 26 | } 27 | 28 | 29 | /* 30 | * Base structure 31 | */ 32 | 33 | html, 34 | body { 35 | height: 100%; 36 | background: #e2e1e0; 37 | } 38 | 39 | .my-card:hover { 40 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 41 | } 42 | body { 43 | display: -ms-flexbox; 44 | display: -webkit-box; 45 | display: flex; 46 | -ms-flex-pack: center; 47 | -webkit-box-pack: center; 48 | justify-content: center; 49 | color: #fff; 50 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); 51 | 52 | } 53 | 54 | .cover-container { 55 | max-width: 42em; 56 | } 57 | 58 | 59 | /* 60 | * Header 61 | */ 62 | .masthead { 63 | margin-bottom: 2rem; 64 | } 65 | 66 | .masthead-brand { 67 | margin-bottom: 0; 68 | -webkit-text-stroke: #333; 69 | } 70 | 71 | .nav-masthead .nav-link { 72 | padding: .25rem 0; 73 | font-weight: 700; 74 | color: #000000; 75 | background-color: transparent; 76 | border-bottom: .25rem solid transparent; 77 | } 78 | 79 | .nav-masthead .nav-link:hover, 80 | .nav-masthead .nav-link:focus { 81 | border-bottom-color: rgba(255, 255, 255, .25); 82 | } 83 | 84 | .nav-masthead .nav-link + .nav-link { 85 | margin-left: 1rem; 86 | } 87 | 88 | .nav-masthead .active { 89 | color: #fff; 90 | border-bottom-color: #fff; 91 | } 92 | 93 | @media (min-width: 48em) { 94 | .masthead-brand { 95 | float: left; 96 | } 97 | .nav-masthead { 98 | float: right; 99 | } 100 | } 101 | 102 | 103 | /* 104 | * Cover 105 | */ 106 | .cover { 107 | padding: 0 1.5rem; 108 | } 109 | .cover .btn-lg { 110 | padding: .75rem 1.25rem; 111 | font-weight: 700; 112 | } 113 | 114 | #h-icon { 115 | color: rgb(255, 0, 0); 116 | text-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); 117 | font-size: 1.5em; 118 | } 119 | /* 120 | * Footer 121 | */ 122 | .mastfoot { 123 | color: rgb(5, 5, 5); 124 | 125 | } -------------------------------------------------------------------------------- /api/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // Error which will be returned on any error regarding the key 14 | var ErrNoOrWrongKeyProvided = errors.New("publishing failed due to no or wrong streamKey provided") 15 | 16 | // PublishRequest maps the post request coming from nginx 17 | // because we are only interested in the provided streamKey the other 18 | // possible fields were omitted. 19 | // from: https://github.com/arut/nginx-rtmp-module/wiki/Directives#on_publish 20 | // HTTP request receives a number of arguments. POST method is used with application/x-www-form-urlencoded MIME type. 21 | // The following arguments are passed to caller: 22 | // 23 | // call=play 24 | // addr - client IP address 25 | // clientid - nginx client id (displayed in log and stat) 26 | // app - application name 27 | // flashVer - client flash version 28 | // swfUrl - client swf url 29 | // tcurl - tcUrl 30 | // pageUrl - client page url 31 | // name - stream name 32 | type PublishRequest struct { 33 | TcURL string `form:"tcurl"` 34 | } 35 | 36 | // OnPublish will receive PublishRequest a like request from nginx on every publish attempt. 37 | // Here we validate the provided key and return 200 if key is correct otherwise 401. 38 | func OnPublish(c *gin.Context) { 39 | apiKey := os.Getenv("STREAM_KEY") 40 | // if no key were set in env variable, disable checking and just return 200 41 | if apiKey == "" { 42 | c.Status(200) 43 | return 44 | } 45 | req := &PublishRequest{} 46 | err := c.Bind(req) 47 | 48 | if err != nil { 49 | c.AbortWithError(http.StatusBadRequest, err) 50 | return 51 | } 52 | key, err := getKeyFromRequest(req) 53 | if err != nil { 54 | c.AbortWithError(http.StatusUnauthorized, err) 55 | return 56 | } 57 | 58 | // is the key OK? 59 | if key == apiKey { 60 | c.Status(200) 61 | return 62 | } 63 | 64 | // key was not ok; disallow publishing 65 | c.AbortWithError(http.StatusUnauthorized, ErrNoOrWrongKeyProvided) 66 | } 67 | 68 | // getKeyFromRequest is a simple helper function which returns the key or an error 69 | // from request 70 | func getKeyFromRequest(req *PublishRequest) (string, error) { 71 | v, err := url.ParseQuery(strings.Split(req.TcURL, "?")[1]) 72 | if err != nil { 73 | return "",err 74 | } 75 | log.Println(v.Encode()) 76 | key := v.Get("key") 77 | if key == "" { 78 | return "", ErrNoOrWrongKeyProvided 79 | } 80 | return key, nil 81 | } 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,linux,intellij 3 | # Edit at https://www.gitignore.io/?templates=go,linux,intellij 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | ### Go Patch ### 20 | /vendor/ 21 | /Godeps/ 22 | 23 | ### Intellij ### 24 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 25 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 26 | ./*/.idea/** 27 | # User-specific stuff 28 | /*/.idea/**/workspace.xml 29 | .idea/**/tasks.xml 30 | .idea/**/usage.statistics.xml 31 | .idea/**/dictionaries 32 | .idea/**/shelf 33 | 34 | # Generated files 35 | .idea/**/contentModel.xml 36 | 37 | # Sensitive or high-churn files 38 | .idea/**/dataSources/ 39 | .idea/**/dataSources.ids 40 | .idea/**/dataSources.local.xml 41 | .idea/**/sqlDataSources.xml 42 | .idea/**/dynamic.xml 43 | .idea/**/uiDesigner.xml 44 | .idea/**/dbnavigator.xml 45 | 46 | # Gradle 47 | .idea/**/gradle.xml 48 | .idea/**/libraries 49 | 50 | # Gradle and Maven with auto-import 51 | # When using Gradle or Maven with auto-import, you should exclude module files, 52 | # since they will be recreated, and may cause churn. Uncomment if using 53 | # auto-import. 54 | # .idea/modules.xml 55 | # .idea/*.iml 56 | # .idea/modules 57 | 58 | # CMake 59 | cmake-build-*/ 60 | 61 | # Mongo Explorer plugin 62 | .idea/**/mongoSettings.xml 63 | 64 | # File-based project format 65 | *.iws 66 | 67 | # IntelliJ 68 | out/ 69 | 70 | # mpeltonen/sbt-idea plugin 71 | .idea_modules/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Cursive Clojure plugin 77 | .idea/replstate.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | fabric.properties 84 | 85 | # Editor-based Rest Client 86 | .idea/httpRequests 87 | 88 | # Android studio 3.1+ serialized cache file 89 | .idea/caches/build_file_checksums.ser 90 | 91 | ### Intellij Patch ### 92 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 93 | 94 | # *.iml 95 | # modules.xml 96 | # .idea/misc.xml 97 | # *.ipr 98 | 99 | # Sonarlint plugin 100 | .idea/sonarlint 101 | 102 | ### Linux ### 103 | *~ 104 | 105 | # temporary files which can be created if a process still has a handle open of a deleted file 106 | .fuse_hidden* 107 | 108 | # KDE directory preferences 109 | .directory 110 | 111 | # Linux trash folder which might appear on any partition or disk 112 | .Trash-* 113 | 114 | # .nfs files are created when an open file is removed but is still being accessed 115 | .nfs* 116 | 117 | # End of https://www.gitignore.io/api/go,linux,intellij -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |