Skip to content

Mynotaurus/PyStreaming

 
 

Repository files navigation

A simple web frontend that works in tandem with nginx-rtmp-module to provide a streaming web site. A compatible RTMP video streaming source such as OBS can send video and audio to an RTMP endpoint served by nginx and the stream will show up for clients on a web page with included stream chat. There are simple moderation commands such as mute available, live presence indication and a layer that ensures stream keys are never exposed publicly.

Setup

Dependencies

This project assumes Python 3.6 or higher, due to (partial) use of type-hints. It assumes a version of nginx with nginx-rtmp-module compiled and available. It assumes a local, modern MySQL instance that you have permission to create databases on. It assumes a linux-like environment to run all of these programs.

Project dependencies are in the requirements.txt file. To install these, run the following command, preferrably in a virtual environment so as to not pollute your system python installation:

python3 -m pip install -r requirements.txt

Initial Setup

Once you have dependencies set up and operational and you have installed the project dependencies into your virtual environment, its time to configure. Edit (or rename) config.yaml and update the database section to point at your MySQL instance. Make sure that you create a database and user that match the settings in your config.yaml file. If you don't know how to do this, see this tutorial.

Once your database is set up, run the following command to hydrate the database:

python3 manage.py --config config.yaml create

If you've renamed your config.yaml file, substitute that new name in the above command. When this is done, you should have the table structure required to stream. You do not have any authorized streamers, however. To add a streamer whose username is "test" and whose stream key is "secretkey", run the following command:

python3 manage.py --config config.yaml addstreamer --user test --key secretkey

You can substitute for your own username and secret key here. Again, if you renamed your config.yaml file, substitute that new name in the above command.

Management

The manage.py script is where you will do all of your database management. Run the script with --help to see available commands. You can add streamers, remove them, list them, upgrade the database on a new version of this code and generate migration scripts if you have modified the database schema and wish to make a pull request. Make sure that you always give it the config.yaml that you have customized in order to operate on the correct database.

Running

You can start the server using the following command:

python3 app.py --config config.yaml --port 12345

This will run the application in production mode. If you want to run the application with more logging and auto-reloading on file changes, add --debug to the arguments. Remember that you can run the above script with --help to see options. Once this is up and running, you can visit the web page at http://127.0.0.1:12345/. If you are setting this up on a remote server, substitute the server's public IP for 127.0.0.1 above. If you have changed the port in the above command, make sure you change the port in the URL as well. You should see a list of all the streamers you've added using the manage.py script. You can click on any of them to go to their streamer page.

nginx Configuration

We will use nginx as the RTMP listening server and HLS transcoder which powers the media portion of this setup. First, you will want to edit your nginx.conf to include a section similar to the following:

rtmp {
    server {
        # Standard RTMP port, where OBS expects to send data.
        listen 1935;

        # Stream chunk size, leave as-is.
        chunk_size 4096;

        # Allow publishing from any IP. We will use the "on_publish" hook for security.
        allow publish all;

        # Deny playback from any IP. This means RTMP clients like VLC cannot connect
        # directly to the server to watch streams.
        deny play all;

        # URL nginx will check when a user tries to stream, to verify the stream key
        # is correct. You should leave this as 127.0.0.1 because nginx should only
        # look on the local host for the auth endpoint.
        on_publish http://127.0.0.1:12345/auth/on_publish;
        on_publish_done http://127.0.0.1:12345/auth/on_publish_done;

        # Create the "/live" endpoint people can stream to.
        application live {
            # Enable this endpoint.
            live on;

            # Disable recording to flv files.
            record off;

            # Enable HLS transcoding. RTMP clients in the browser no longer exist.
            hls on;

            # Where we will put the HLS files and playlists.
            hls_path /path/to/hls/;

            # How long each segment should be. If you are experiencing large stream
            # delays, you can reduce this down to 1s.
            hls_fragment 3s;

            # How much playback buffer we keep around. If you are experiencing large
            # stream delays, you can reduce this down to 10s. Be sure to adjust the
            # hls_playlist_length option in config.yaml to match!
            hls_playlist_length 30s;
        }
    }
}

There are a few things you will want to customize in that above section based on your setup. First is the port in the on_publish section. The port must match the port you run your application with. If you changed your --port setting when you ran app.py, make sure to also change the port in on_publish so that nginx can validate streamer permissions. The path given in hls_path should be readable and writeable by the nginx user and the python server user, and should match the path in your config.yaml hls_dir setting. nginx will put transcoded HLS files in this directory and this is where the python application will look to find stream information.

If you've set everything up correctly and are running app.py as well as nginx, you should be able to point OBS at rtmp://127.0.0.1/live and start streaming. Make sure your stream key matches what you added using manage.py above! If you are setting this up on a remote server, substitute the server's public IP for 127.0.0.1 in the URL above. If everything is set up right, you should see the stream go live on the web interface.

Transcoding Multiple Qualities

By default, the above setup will deliver a single quality stream to all viewers that is based on your source configuration (most-likely in OBS or another streaming software). If you want to provide multiple quality streams for viewers with slower internet connections you will need to modify your nginx.conf as well as your config.yaml setup. In this alternate configuration you will use ffmpeg to translate the incoming stream to multiple streams on the fly. First you will want to edit your nginx.conf to include a section similar to the following (instead of the above simpler config):

rtmp {
    server {
        # Standard RTMP port, where OBS expects to send data.
        listen 1935;

        # Stream chunk size, leave as-is.
        chunk_size 4096;

        # Create the "/live" endpoint people can stream to.
        application live {
            # Allow publishing from any IP. We will use the "on_publish" hook for security.
            allow publish all;

            # Allow localhost to play this stream back over RTML. This is important as this is
            # how ffmpeg will transcode the stream.
            allow play 127.0.0.1;

            # Enable this endpoint.
            live on;

            # Disable recording to flv files.
            record off;

            # URL nginx will check when a user tries to stream, to verify the stream key
            # is correct. You should leave this as 127.0.0.1 because nginx should only
            # look on the local host for the auth endpoint.
            on_publish http://127.0.0.1:12345/auth/on_publish;
            on_publish_done http://127.0.0.1:12345/auth/on_publish_done;

            # Push transcoded streams to the "/hls" endpoint. Feel free to change the
            # parameters of the various transcoded streams, add or drop them at will.
            # If you don't wish to include the original quality stream you can drop the
            # line which copies video/audio to the "/hls" endpoint.
            exec ffmpeg -i rtmp://127.0.0.1:1935/$app/$name
            -c copy -f flv rtmp://127.0.0.1:1935/hls/$name_Original
            -c:v libx264 -acodec copy -b:v 2304k -vf "scale=1080:trunc(ow/a/2)*2" -tune zerolatency -preset veryfast -crf 23 -g 60 -hls_list_size 0 -f flv rtmp://127.0.0.1:1935/hls/$name_1080P
            -c:v libx264 -acodec copy -b:v 768k -vf "scale=720:trunc(ow/a/2)*2" -tune zerolatency -preset veryfast -crf 23 -g 60 -hls_list_size 0 -f flv rtmp://127.0.0.1:1935/hls/$name_720P
            -c:v libx264 -acodec copy -b:v 256k -vf "scale=480:trunc(ow/a/2)*2" -tune zerolatency -preset veryfast -crf 23 -g 60 -hls_list_size 0 -f flv rtmp://127.0.0.1:1935/hls/$name_480P;
        }

        # Create the "/hls" endpoint used by ffmpeg to push transcoded streams.
        application hls {
            # Only allow publishing from localhost, specifically from ffmpeg from above.
            allow publish 127.0.0.1;

            # Deny playback from any IP. This means RTMP clients like VLC cannot connect
            # directly to the server to watch streams.
            deny play all;

            # Enable this endpoint.
            live on;

            # Disable recording to flv files.
            record off;

            # Enable HLS transcoding. RTMP clients in the browser no longer exist.
            hls on;

            # Where we will put the HLS files and playlists.
            hls_path /path/to/hls/;

            # How long each segment should be. If you are experiencing large stream
            # delays, you can reduce this down to 1s.
            hls_fragment 3s;

            # How much playback buffer we keep around. If you are experiencing large
            # stream delays, you can reduce this down to 10s. Be sure to adjust the
            # hls_playlist_length option in config.yaml to match!
            hls_playlist_length 30s;
        }
    }
}

You'll notice that compared to the simpler setup, we split the RTMP server into two pieces. The first piece handles authentication based on stream key and is the endpoint you will stream to. Its responsible for launching ffmpeg to transcode streams. The second piece listens for the transcoded streams and is responsible for writing out the HLS file fragments that make the whole thing work. In both the simpler and more complex setup, you will be pointing your streaming setup at the /live RTMP endpoint.

In order for this to work with the streaming frontend you also need to edit your config.yaml to specify the qualities you support. Without this, it will assume no suffix for stream keys and will only work with the simpler nginx.conf. For the demo nginx.conf laid out just above, you will want a similar section in your config.yaml that looks like the following:

video_qualities:
    - Original
    - 1080P
    - 720P
    - 480P

Notice that the suffixes we provided after $name for the original and transcoded streams are all listed here. When you set the software up in this manner, a configuration gear will be visible on the stream for viewers where they can choose a quality. You'll want to verify with a test stream to ensure that your streaming server can handle the load of transcoding. If it can't, you'll notice that all qualities will buffer randomly and your CPU usage will be too high for the number of cores. If your server can't keep up, choose fewer transcodes and try again.

Running Behind nginx

Its a good idea to serve up the web interface behind nginx instead of directly. This allows you to set up things such as SSL, provide the weight of nginx to deliver HLS files to clients and generally makes your setup more robust. First, add a site to sites-available with contents similar to the following:

server {
    # The domain name this nginx config serves.
    server_name coolstreamingsite.com;

    # Listen on port 80 (standard web port).
    listen 80;

    # Don't advertise nginx.
    server_tokens off;

    # Set up the root to point at our server.
    location / {
        # Make sure to pass on IP information.
        include proxy_params;

        # Pass traffic to app.py.
        proxy_pass http://127.0.0.1:12345/;
    }

    # Set up websockets for stream chat.
    location /socket.io {
        # Leave all of this as-is.
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # Pass websockets to app.py
        proxy_pass http://127.0.0.1:12345/socket.io;
    }

    # Set up HLS directory so nginx can directly handle stream chunks.
    location /hls {
        # Leave all of this as-is.
        add_header Cache-Control no-cache;
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length';

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }

        # The root directory which holds the "hls" subdirectory we are putting
        # HLS files in. The full directory will be this root and the location
        # concatenated. In this case "/path/to/hls"
        root /path/to;

        # Don't allow listing of the directory, to keep stream keys secret.
        autoindex off;
    }

    location /static {
        # Make sure to update this to the directory you're running app.py out
        # of as it allows nginx to handle static resources, taking strain off
        # of the python application.
        root /location/of/this/repo;

        # No need to show the world our dirty laundry.
        autoindex off;
    }
}

There are a few things you wil want to modify in this file. For starters, if you aren't using DNS, get rid of the server_name entry. If you are, make sure this setting matches your domain name. Next, its important that the port in both proxy_pass settings matches the port you ran your app.py script with. This should match the port in your nginx.conf as well. Finally, make sure the directory you chose to put HLS files in matches the root option. nginx is a bit weird here, since /hls is a subdirectory, you give it the parent of the option you specified in your nginx.conf and config.yaml files. So if you chose /path/to/hls as your HLS file location, you would put /path/to in the root option above. Make sure that the directory you chose is readable/writeable by both the nginx user and the user you are running app.py under.

Once you set this up, you will want to run app.py again, this time with a slightly different set of arguments:

python3 app.py --config config.yaml --port 12345 --nginx-proxy 1

The --nginx-proxy command says how many hops through nginx we had to make before we hit app.py. This enables us to correctly determine the IP address of remote clients without introducing a security hole. If you set everything up correctly you will be able to visit http://coolstreamingsite.com and view your streams! Note that you should change that domain if you are hosing this under a different DNS entry. If you do not have a DNS entry, just use your server's public IP address instead.

Port forwarding and Firewall

In order to reach the server from outside, open and forward ports 1935 and 80. If you set up SSL, open and forward port 443 instead of port 80. Do not open or forward the port you ran app.py on as nginx takes care of proxying requests for us. Now, you will be able to point OBS at rtmp://coolstreamingsite.com/live to stream! Make sure to change the domain to the one you are using for your server, or if you are not using DNS, the public IP of the server.

Streaming

Anyone that visits a streamer's page can join the chat and watch. There is currently no authentication for normal chatters. To join as the stream host, use the same name as the stream. You will be asked for your stream key as a password to authenticate. So, if you were using the user test from the set up example, you could visit http://coolstreamingsite.com/test and join the chat as test. You will be asked to provide the stream key secretkey in order to authenticate. Once you are connected to chat, type /help into the chat box to see available commands. You can assign moderators to help you moderate if you wish. Stream hosts and moderators can mute users, and stream hosts can change the description. All users can chat and use actions with /me.

Future Enhancements

  • Kick and ban fron chat feature based on client IP. Not currently necessary but might become so.
  • Persistent username/account feature. Not sure how desirable this is, but its an option.
  • Word filtering support for chat. Not currently necessary but I'm sure it will end up being needed.
  • Rate limiting for message sends in chat. Not currently necessary but I'm sure that it will end up being needed.
  • Better front page with streamer highlights and such.
  • Better mobile support across the board.

About

Simple web server that works with nginx to allow RTMP streaming on the web.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 56.5%
  • HTML 28.8%
  • JavaScript 9.4%
  • CSS 3.9%
  • Other 1.4%