├── pics ├── bd-full.png ├── lat-lhls.jpeg ├── lat-lhls-srt.jpeg ├── bd-full-no-cdn.png └── bd-full-no-cdn-gray.png ├── README.md └── scripts ├── transcoding-multirendition-srt.sh └── transcoding-multirendition-rtmp.sh /pics/bd-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordicenzano/lhls-simple-live-platform/HEAD/pics/bd-full.png -------------------------------------------------------------------------------- /pics/lat-lhls.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordicenzano/lhls-simple-live-platform/HEAD/pics/lat-lhls.jpeg -------------------------------------------------------------------------------- /pics/lat-lhls-srt.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordicenzano/lhls-simple-live-platform/HEAD/pics/lat-lhls-srt.jpeg -------------------------------------------------------------------------------- /pics/bd-full-no-cdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordicenzano/lhls-simple-live-platform/HEAD/pics/bd-full-no-cdn.png -------------------------------------------------------------------------------- /pics/bd-full-no-cdn-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordicenzano/lhls-simple-live-platform/HEAD/pics/bd-full-no-cdn-gray.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lhls-simple-live-platform 2 | This readme shows how to build a very simple live streaming platform based on open source tools with a glass to glass latency around 2s. 3 | 4 | To learn more about it see the presentation at syd\ on 2020/06/24: 5 | - [Slides](https://slides.com/jordicenzano/deck-973aed) 6 | - [Demo Video](https://youtu.be/rTaL-9XL03g) 7 | 8 | ## Block diagram 9 | ![Block diagram](./pics/bd-full-no-cdn-gray.png) 10 | 11 | ## Installation 12 | - Launch EC2 13 | - Type: 14 | - t2.small (for transmuxing) 15 | - t2.xlarge (for transcoding from 720p to 720p and 480p) 16 | - Security groups 17 | - Open Inbound TCP 1935 for everybody (RTMP) 18 | - - Open Inbound UDP 1935 for everybody (SRT) 19 | - Open Inbound 9094 for everybody (HTTP) 20 | - Update the machine `sudo yum update -y` 21 | - Install git: `sudo yum install git -y` 22 | - (optional) Install tmux: `sudo yum install tmux -y` 23 | - Install (compile) ffmpeg 24 | - You can use this [script](https://github.com/jordicenzano/ffmpeg-compile-centos-amazon-linux) or [source code](https://trac.ffmpeg.org/wiki/CompilationGuide) 25 | - Install DejaVu fonts (to do the overlay on transcoding): `sudo yum install dejavu-sans-fonts -y` 26 | - Install this repo 27 | ```bash 28 | cd ~ 29 | git clone https://github.com/jordicenzano/lhls-simple-live-platform.git 30 | ``` 31 | - Install GO, see [instructions](https://golang.org/doc/install) 32 | - From your home dir `cd ~` install and compile [go-ts-segmenter](https://github.com/jordicenzano/go-ts-segmenter) 33 | ```bash 34 | go get github.com/jordicenzano/go-ts-segmenter 35 | ``` 36 | - From your home dir `cd ~` install and compile [go-chunked-streaming-server](https://github.com/mjneil/go-chunked-streaming-server) 37 | ```bash 38 | go get github.com/mjneil/go-chunked-streaming-server 39 | ``` 40 | 41 | ## Usage (RTMP source) 42 | - Create a shell to the EC2 machine (`tmux` recommended), and start webserver in HTTPS 43 | ```bash 44 | cd ~/go/bin/ 45 | ./go-chunked-streaming-server 46 | ``` 47 | - Create ANOTHER shell to the EC2 machine (`tmux` recommended), start RTMP server + segmenter with a multirendion transcoding configuration 48 | ```bash 49 | cd ~/lhls-simple-live-platform/scripts/ 50 | ./transcoding-multirendition-rtmp.sh live 51 | ``` 52 | - Open your favorite RTMP client: [OBS](https://obsproject.com/), [Wirecast](https://www.telestream.net/wirecast/overview.htm), [Elemental](https://aws.amazon.com/elemental-live/), [Wowza Clearcaster](https://www.wowza.com/products/clearcaster), [ffmpeg](https://ffmpeg.org/), etc 53 | - Configure the RTMP URL as: `rtmp://[PUBLIC-IP-EC2]:1935/live/stream` 54 | - Recommended short GOP (1s) and (if possible) activate "zerolatency" video encoding mode 55 | - Start streaming 56 | - Tested with following players: [Safari](https://www.apple.com/safari/), [Quicktime](https://support.apple.com/en-us/HT201066) and [ffplay](https://ffmpeg.org/ffplay.html)) 57 | - Use this URL: `http://[PUBLIC-IP-EC2]:9094/mrrtmp/playlist.m3u8` 58 | 59 | - Example glass to glass latency with this set up: **2.01s** 60 | ![Glass to glass latency](./pics/lat-lhls.jpeg) 61 | 62 | ## Usage (SRT source) 63 | - Create a shell to the EC2 machine (`tmux` recommended), and start webserver in HTTPS 64 | ```bash 65 | cd ~/go/bin/ 66 | ./go-chunked-streaming-server 67 | ``` 68 | - Create ANOTHER shell to the EC2 machine (`tmux` recommended), start SRT server + segmenter with a multirendion transcoding configuration 69 | ```bash 70 | cd ~/lhls-simple-live-platform/scripts/ 71 | ./transcoding-multirendition-srt.sh live 72 | ``` 73 | - Open your favorite SRT client: [OBS](https://obsproject.com/), [Wowza Clearcaster](https://www.wowza.com/products/clearcaster), [ffmpeg](https://ffmpeg.org/), etc 74 | - Configure the SRT URL as: `srt://[PUBLIC-IP-EC2]:1935` 75 | - Recommended short GOP (1s) and (if possible) activate "zerolatency" video encoding mode 76 | - Start streaming 77 | - Tested with following players: [Safari](https://www.apple.com/safari/), [Quicktime](https://support.apple.com/en-us/HT201066) and [ffplay](https://ffmpeg.org/ffplay.html)) 78 | - Use this URL: `http://[PUBLIC-IP-EC2]:9094/mrsrt/playlist.m3u8` 79 | 80 | - Example glass to glass latency with this set up: **2.54s** 81 | ![Glass to glass latency](./pics/lat-lhls-srt.jpeg) 82 | 83 | ## Notes 84 | - This is JUST A PROOF OF CONCEPT / PROTOTYPE do not use it in production 85 | - Example of known problem: If the encoder reconnect for any reason, the live stream finishes 86 | - There is a lot going on about how to do ABR when you are sending data at "media speed" and NOT at "cable speed". So for multirendtion and chunk transfer you should expect regular players going to the lower BW lane 87 | - [go-chunked-streaming-server](https://github.com/mjneil/go-chunked-streaming-server) does not send `max-age` headers, so if you want to **add a CDN on top of this prototype to scale this platform up** you need to set up the expiration manually 88 | -------------------------------------------------------------------------------- /scripts/transcoding-multirendition-srt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 1 ]; then 4 | echo "Use ./transcoding-multirendition-srt.sh test/live [SRTPort] [HLSOutHostPort]" 5 | echo "test/live: In test generated a test signal" 6 | echo "SRTPort: SRT listener local UDP port (default: 1935)" 7 | echo "HLSOutHostPort: Host and to send HLS data (default: \"localhost:9094\")" 8 | echo "Example: ./transcoding-multirendition-srt.sh live 1935 \"stream\" \"localhost:9094\"" 9 | exit 1 10 | fi 11 | 12 | MODE="${1}" 13 | SRT_PORT="${2:-"1935"}" 14 | HOST_DST="${3:-"localhost:9094"}" 15 | 16 | PATH_NAME="mrsrt" 17 | STREAM_NAME_720p="720p" 18 | STREAM_NAME_480p="480p" 19 | BASE_DIR="../results/${PATH_NAME}" 20 | LOGS_DIR="../logs" 21 | GO_BINARY_DIR="~/go/bin" 22 | eval TS_SEGMENTER_BIN="$GO_BINARY_DIR/go-ts-segmenter" 23 | 24 | # Check segmenter binary 25 | if [ ! -f $TS_SEGMENTER_BIN ]; then 26 | echo "$TS_SEGMENTER_BIN does not exist." 27 | exit 1 28 | fi 29 | 30 | # Clean up 31 | echo "Restarting ${BASE_DIR} directory" 32 | rm -rf $BASE_DIR/* 33 | mkdir -p $BASE_DIR 34 | mkdir -p $LOGS_DIR 35 | 36 | # Create master playlist (this should be created after 1st chunk is uploaded) 37 | # Assuming source is 1280x720@6Mbps (or better) 38 | # Creating 720p@6Mbps and 480p@3Mbps 39 | echo "Creating master playlist manifest (playlist.m3u8)" 40 | echo "#EXTM3U" > $BASE_DIR/playlist.m3u8 41 | echo "#EXT-X-VERSION:3" >> $BASE_DIR/playlist.m3u8 42 | echo "#EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=1280x720" >> $BASE_DIR/playlist.m3u8 43 | echo "$STREAM_NAME_720p.m3u8" >> $BASE_DIR/playlist.m3u8 44 | echo "#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=854x480" >> $BASE_DIR/playlist.m3u8 45 | echo "$STREAM_NAME_480p.m3u8" >> $BASE_DIR/playlist.m3u8 46 | 47 | # Upload master playlist 48 | curl "http://${HOST_DST}/${PATH_NAME}/playlist.m3u8" -H "Content-Type: application/vnd.apple.mpegurl" --upload-file $BASE_DIR/playlist.m3u8 49 | 50 | # Select font path based in OS 51 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 52 | FONT_PATH='/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf' 53 | elif [[ "$OSTYPE" == "darwin"* ]]; then 54 | FONT_PATH='/Library/Fonts/Arial.ttf' 55 | fi 56 | 57 | # Creates pipes 58 | FIFO_FILENAME_720p="fifo-$STREAM_NAME_720p" 59 | mkfifo $BASE_DIR/$FIFO_FILENAME_720p 60 | FIFO_FILENAME_480p="fifo-$STREAM_NAME_480p" 61 | mkfifo $BASE_DIR/$FIFO_FILENAME_480p 62 | 63 | # Creates hls producers 64 | cat "$BASE_DIR/$FIFO_FILENAME_720p" | $TS_SEGMENTER_BIN -logsPath "$LOGS_DIR/segmenter720p.log" -dstPath ${PATH_NAME} -manifestDestinationType 2 -mediaDestinationType 2 -targetDur 1 -lhls 3 -chunksBaseFilename ${STREAM_NAME_720p}_ -chunklistFilename ${STREAM_NAME_720p}.m3u8 & 65 | PID_720p=$! 66 | echo "Started go-ts-segmenter for $STREAM_NAME_720p as PID $PID_720p" 67 | cat "$BASE_DIR/$FIFO_FILENAME_480p" | $TS_SEGMENTER_BIN -logsPath "$LOGS_DIR/segmenter480p.log" -dstPath ${PATH_NAME} -manifestDestinationType 2 -mediaDestinationType 2 -targetDur 1 -lhls 3 -chunksBaseFilename ${STREAM_NAME_480p}_ -chunklistFilename ${STREAM_NAME_480p}.m3u8 & 68 | PID_480p=$! 69 | echo "Started go-ts-segmenter for $STREAM_NAME_480p as PID $PID_480p" 70 | 71 | if [[ "$MODE" == "test" ]]; then 72 | # Start test signal 73 | # GOP size = 30f @ 30 fps = 1s 74 | ffmpeg -hide_banner -y \ 75 | -f lavfi -re -i smptebars=duration=36000:size=1280x720:rate=30 \ 76 | -f lavfi -i sine=frequency=1000:duration=36000:sample_rate=48000 -pix_fmt yuv420p \ 77 | -s 1280x720 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 720p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 78 | -c:v libx264 -tune zerolatency -b:v 6000k -g 30 -preset ultrafast \ 79 | -c:a aac -b:a 48k \ 80 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_720p" \ 81 | -s 854x480 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 480p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 82 | -c:v libx264 -tune zerolatency -b:v 3000k -g 30 -preset ultrafast \ 83 | -c:a aac -b:a 48k \ 84 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_480p" 85 | else 86 | # Start multilane transcoder from SRT to TS 87 | ffmpeg -hide_banner -y \ 88 | -i "srt://0.0.0.0:$SRT_PORT?mode=listener&latency=120000" \ 89 | -s 1280x720 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 720p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 90 | -c:v libx264 -tune zerolatency -b:v 6000k -g 30 -preset ultrafast \ 91 | -c:a aac -b:a 48k \ 92 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_720p" \ 93 | -s 854x480 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 480p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 94 | -c:v libx264 -tune zerolatency -b:v 3000k -g 30 -preset ultrafast \ 95 | -c:a aac -b:a 48k \ 96 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_480p" 97 | fi 98 | 99 | # Clean up: Stop processes 100 | # If the input stream stops the segmenter processes exists themselves 101 | # kill $PID_720p 102 | # kill $PID_480p 103 | -------------------------------------------------------------------------------- /scripts/transcoding-multirendition-rtmp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 1 ]; then 4 | echo "Use ./transcoding-multirendition-rtmp.sh test/live [RTMPPort] [RTMPApp] [RTMPStream] [HLSOutHostPort]" 5 | echo "test/live: In test generates test signal" 6 | echo "RTMPPort: RTMP local port (default: 1935)" 7 | echo "RTMPPort: RTMP app name (default: \"live\")" 8 | echo "RTMPPort: RTMP stream name (default: \"stream\")" 9 | echo "HLSOutHostPort: Host and to send HLS data (default: \"localhost:9094\")" 10 | echo "Example: ./transcoding-multirendition-rtmp.sh live 1935 \"live\" \"stream\" \"localhost:9094\"" 11 | exit 1 12 | fi 13 | 14 | MODE="${1}" 15 | RTMP_PORT="${2:-"1935"}" 16 | RTMP_APP="${3:-"live"}" 17 | RTMP_STREAM="${4:-"stream"}" 18 | HOST_DST="${5:-"localhost:9094"}" 19 | 20 | PATH_NAME="mrrtmp" 21 | STREAM_NAME_720p="720p" 22 | STREAM_NAME_480p="480p" 23 | BASE_DIR="../results/${PATH_NAME}" 24 | LOGS_DIR="../logs" 25 | GO_BINARY_DIR="~/go/bin" 26 | eval TS_SEGMENTER_BIN="$GO_BINARY_DIR/go-ts-segmenter" 27 | 28 | # Check segmenter binary 29 | if [ ! -f $TS_SEGMENTER_BIN ]; then 30 | echo "$TS_SEGMENTER_BIN does not exist." 31 | exit 1 32 | fi 33 | 34 | # Clean up 35 | echo "Restarting ${BASE_DIR} directory" 36 | rm -rf $BASE_DIR/* 37 | mkdir -p $BASE_DIR 38 | mkdir -p $LOGS_DIR 39 | 40 | # Create master playlist (this should be created after 1st chunk is uploaded) 41 | # Assuming source is 1280x720@6Mbps (or better) 42 | # Creating 720p@6Mbps and 480p@3Mbps 43 | echo "Creating master playlist manifest (playlist.m3u8)" 44 | echo "#EXTM3U" > $BASE_DIR/playlist.m3u8 45 | echo "#EXT-X-VERSION:3" >> $BASE_DIR/playlist.m3u8 46 | echo "#EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=1280x720" >> $BASE_DIR/playlist.m3u8 47 | echo "$STREAM_NAME_720p.m3u8" >> $BASE_DIR/playlist.m3u8 48 | echo "#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=854x480" >> $BASE_DIR/playlist.m3u8 49 | echo "$STREAM_NAME_480p.m3u8" >> $BASE_DIR/playlist.m3u8 50 | 51 | # Upload master playlist 52 | curl "http://${HOST_DST}/${PATH_NAME}/playlist.m3u8" -H "Content-Type: application/vnd.apple.mpegurl" --upload-file $BASE_DIR/playlist.m3u8 53 | 54 | # Select font path based in OS 55 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 56 | FONT_PATH='/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf' 57 | elif [[ "$OSTYPE" == "darwin"* ]]; then 58 | FONT_PATH='/Library/Fonts/Arial.ttf' 59 | fi 60 | 61 | # Creates pipes 62 | FIFO_FILENAME_720p="fifo-$STREAM_NAME_720p" 63 | mkfifo $BASE_DIR/$FIFO_FILENAME_720p 64 | FIFO_FILENAME_480p="fifo-$STREAM_NAME_480p" 65 | mkfifo $BASE_DIR/$FIFO_FILENAME_480p 66 | 67 | # Creates hls producers 68 | cat "$BASE_DIR/$FIFO_FILENAME_720p" | $TS_SEGMENTER_BIN -logsPath "$LOGS_DIR/segmenter720p.log" -dstPath ${PATH_NAME} -manifestDestinationType 2 -mediaDestinationType 2 -targetDur 1 -lhls 3 -chunksBaseFilename ${STREAM_NAME_720p}_ -chunklistFilename ${STREAM_NAME_720p}.m3u8 & 69 | PID_720p=$! 70 | echo "Started go-ts-segmenter for $STREAM_NAME_720p as PID $PID_720p" 71 | cat "$BASE_DIR/$FIFO_FILENAME_480p" | $TS_SEGMENTER_BIN -logsPath "$LOGS_DIR/segmenter480p.log" -dstPath ${PATH_NAME} -manifestDestinationType 2 -mediaDestinationType 2 -targetDur 1 -lhls 3 -chunksBaseFilename ${STREAM_NAME_480p}_ -chunklistFilename ${STREAM_NAME_480p}.m3u8 & 72 | PID_480p=$! 73 | echo "Started go-ts-segmenter for $STREAM_NAME_480p as PID $PID_480p" 74 | 75 | if [[ "$MODE" == "test" ]]; then 76 | # Start test signal 77 | # GOP size = 30f @ 30 fps = 1s 78 | ffmpeg -hide_banner -y \ 79 | -f lavfi -re -i smptebars=duration=36000:size=1280x720:rate=30 \ 80 | -f lavfi -i sine=frequency=1000:duration=36000:sample_rate=48000 -pix_fmt yuv420p \ 81 | -s 1280x720 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 720p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 82 | -c:v libx264 -tune zerolatency -b:v 6000k -g 30 -preset ultrafast \ 83 | -c:a aac -b:a 48k \ 84 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_720p" \ 85 | -s 854x480 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 480p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 86 | -c:v libx264 -tune zerolatency -b:v 3000k -g 30 -preset ultrafast \ 87 | -c:a aac -b:a 48k \ 88 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_480p" 89 | else 90 | # Start multilane transcoder from RTMP to TS 91 | ffmpeg -hide_banner -y \ 92 | -listen 1 -i "rtmp://0.0.0.0:$RTMP_PORT/$RTMP_APP/$RTMP_STREAM" \ 93 | -s 1280x720 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 720p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 94 | -c:v libx264 -tune zerolatency -b:v 6000k -g 30 -preset ultrafast \ 95 | -c:a aac -b:a 48k \ 96 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_720p" \ 97 | -s 854x480 -vf "drawtext=fontfile=$FONT_PATH:text=\'RENDITION 480p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\':x=10:y=350:fontsize=30:fontcolor=pink:box=1:boxcolor=0x00000099" \ 98 | -c:v libx264 -tune zerolatency -b:v 3000k -g 30 -preset ultrafast \ 99 | -c:a aac -b:a 48k \ 100 | -f mpegts "$BASE_DIR/$FIFO_FILENAME_480p" 101 | fi 102 | 103 | # Clean up: Stop processes 104 | # If the input stream stops the segmenter processes exists themselves 105 | # kill $PID_720p 106 | # kill $PID_480p 107 | --------------------------------------------------------------------------------