From b675023804c70c3ffe672eb86fa38e91dd1e17a1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 12 Oct 2020 23:11:02 +0200 Subject: [PATCH 01/26] Prepare ascii art quality --- stream/webrtc/ingest.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 144a677..9ebe430 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -3,6 +3,7 @@ package webrtc import ( "bufio" + "fmt" "io" "log" "net" @@ -51,7 +52,8 @@ func ingestFrom(inputChannel chan srt.Packet) { "-auto-alt-ref", "1", "-f", "rtp", "rtp://127.0.0.1:5004", "-vn", "-acodec", "libopus", "-cpu-used", "5", "-deadline", "1", "-qmin", "10", "-qmax", "42", "-error-resilient", "1", "-auto-alt-ref", "1", - "-f", "rtp", "rtp://127.0.0.1:5005") + "-f", "rtp", "rtp://127.0.0.1:5005", + "-an", "-f", "rawvideo", "-vf", "scale=32x18", "-pix_fmt", "gray", "pipe:1") input, err := ffmpeg.StdinPipe() if err != nil { @@ -62,6 +64,10 @@ func ingestFrom(inputChannel chan srt.Packet) { if err != nil { panic(err) } + output, err := ffmpeg.StdoutPipe() + if err != nil { + panic(err) + } if err := ffmpeg.Start(); err != nil { panic(err) @@ -99,6 +105,9 @@ func ingestFrom(inputChannel chan srt.Packet) { } }() + // Receive ascii + go asciiArt(output, videoTracks[srtPacket.StreamName]) + // Receive audio go func() { for { @@ -156,3 +165,27 @@ func ingestFrom(inputChannel chan srt.Packet) { } } } + +func asciiChar(pixel byte) string { + asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."} + return asciiChars[pixel/25] +} + +func asciiArt(reader io.Reader, videoTracks []*webrtc.Track) { + + buff := make([]byte, 2048) + for { + n, _ := reader.Read(buff) + if n == 0 { + break + } + for j := 0; j < 18; j++ { + for i := 0; i < 32; i++ { + pixel := buff[32*j+i] + fmt.Print(asciiChar(pixel)) + } + fmt.Println() + } + fmt.Println() + } +} From e640450d988c5ddcad951612f79da6aa732a8328 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 12 Oct 2020 23:39:26 +0200 Subject: [PATCH 02/26] Start telnet connection to send ASCII Art stream --- stream/telnet/telnet.go | 68 +++++++++++++++++++++++++++++++++++++++++ stream/webrtc/ingest.go | 28 ++--------------- 2 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 stream/telnet/telnet.go diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go new file mode 100644 index 0000000..ac58aac --- /dev/null +++ b/stream/telnet/telnet.go @@ -0,0 +1,68 @@ +// Package telnet provides some fancy tools, like an ASCII-art stream. +package telnet + +import ( + "io" + "log" + "net" + "time" +) + +func asciiChar(pixel byte) string { + asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."} + return asciiChars[pixel/25] +} + +// ServeAsciiArt starts a telnet server that send all packets as ASCII Art +func ServeAsciiArt(reader io.Reader) { + listener, err := net.Listen("tcp", ":4242") + if err != nil { + log.Printf("Error while listening to the port 4242: %s", err) + return + } + + currentMessage := "" + + go func() { + for { + s, err := listener.Accept() + if err != nil { + log.Printf("Error while accepting TCP socket: %s", s) + continue + } + go func(s net.Conn) { + for { + n, err := s.Write([]byte(currentMessage)) + if err != nil { + log.Printf("Error while sending TCP data: %s", err) + _ = s.Close() + break + } + if n == 0 { + _ = s.Close() + break + } + time.Sleep(50 * time.Millisecond) + } + }(s) + } + }() + + buff := make([]byte, 2048) + header := "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + for { + n, _ := reader.Read(buff) + if n == 0 { + break + } + imageStr := "" + for j := 0; j < 18; j++ { + for i := 0; i < 32; i++ { + pixel := buff[32*j+i] + imageStr += asciiChar(pixel) + asciiChar(pixel) + } + imageStr += "\n" + } + currentMessage = header + imageStr + } +} diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 9ebe430..3ba4cb5 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -3,7 +3,7 @@ package webrtc import ( "bufio" - "fmt" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "io" "log" "net" @@ -106,7 +106,7 @@ func ingestFrom(inputChannel chan srt.Packet) { }() // Receive ascii - go asciiArt(output, videoTracks[srtPacket.StreamName]) + go telnet.ServeAsciiArt(output) // Receive audio go func() { @@ -165,27 +165,3 @@ func ingestFrom(inputChannel chan srt.Packet) { } } } - -func asciiChar(pixel byte) string { - asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."} - return asciiChars[pixel/25] -} - -func asciiArt(reader io.Reader, videoTracks []*webrtc.Track) { - - buff := make([]byte, 2048) - for { - n, _ := reader.Read(buff) - if n == 0 { - break - } - for j := 0; j < 18; j++ { - for i := 0; i < 32; i++ { - pixel := buff[32*j+i] - fmt.Print(asciiChar(pixel)) - } - fmt.Println() - } - fmt.Println() - } -} From 61ae490a5df7613f0da4e5bac16d28bdf68dc962 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 00:15:23 +0200 Subject: [PATCH 03/26] Make telnet output configurable --- docs/ghostream.example.yml | 17 +++++++++++ internal/config/config.go | 9 ++++++ main.go | 2 ++ stream/telnet/telnet.go | 61 ++++++++++++++++++++++++++++---------- stream/webrtc/ingest.go | 3 +- 5 files changed, 75 insertions(+), 17 deletions(-) diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index d28a95d..3b7d628 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -68,6 +68,23 @@ srt: # Max number of active SRT connections #maxClients: 64 +## Telnet server ## +# The telnet server receive the stream and emit the stream as ASCII-art. +telnet: + # By default, this easter egg is disabled. + # You must enable it to use it. + #enable: false + + #listenAddress: :4242 + + # Size is in characters. It is recommended to keep a 16x9 format. + #width: 80 + #height: 45 + + # Time in milliseconds that we should sleep between two images. By default, 20 FPS. Displaying text takes time... + #delay: 50 + + ## Web server ## # The web server serves a WebRTC player. web: diff --git a/internal/config/config.go b/internal/config/config.go index 96e8606..eab8b79 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "bytes" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "log" "net" "strings" @@ -25,6 +26,7 @@ type Config struct { Forwarding forwarding.Options Monitoring monitoring.Options Srt srt.Options + Telnet telnet.Options Web web.Options WebRTC webrtc.Options } @@ -56,6 +58,13 @@ func New() *Config { ListenAddress: ":9710", MaxClients: 64, }, + Telnet: telnet.Options{ + Enabled: false, + ListenAddress: ":4242", + Width: 80, + Height: 45, + Delay: 50, + }, Web: web.Options{ Enabled: true, Favicon: "/static/img/favicon.svg", diff --git a/main.go b/main.go index deb5a12..36815b2 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( + "gitlab.crans.org/nounous/ghostream/stream/telnet" "log" "gitlab.crans.org/nounous/ghostream/auth" @@ -50,6 +51,7 @@ func main() { go forwarding.Serve(forwardingChannel, cfg.Forwarding) go monitoring.Serve(&cfg.Monitoring) go srt.Serve(&cfg.Srt, authBackend, forwardingChannel, webrtcChannel) + go telnet.Serve(&cfg.Telnet) go web.Serve(remoteSdpChan, localSdpChan, &cfg.Web) go webrtc.Serve(remoteSdpChan, localSdpChan, webrtcChannel, &cfg.WebRTC) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index ac58aac..8ad53df 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -8,20 +8,34 @@ import ( "time" ) -func asciiChar(pixel byte) string { - asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."} - return asciiChars[pixel/25] +var ( + Cfg *Options + currentMessage *string +) + +// Options holds telnet package configuration +type Options struct { + Enabled bool + ListenAddress string + Width int + Height int + Delay int } -// ServeAsciiArt starts a telnet server that send all packets as ASCII Art -func ServeAsciiArt(reader io.Reader) { - listener, err := net.Listen("tcp", ":4242") - if err != nil { - log.Printf("Error while listening to the port 4242: %s", err) +func Serve(config *Options) { + Cfg = config + + if !Cfg.Enabled { return } - currentMessage := "" + listener, err := net.Listen("tcp", Cfg.ListenAddress) + if err != nil { + log.Printf("Error while listening to the address %s: %s", Cfg.ListenAddress, err) + return + } + + currentMessage = new(string) go func() { for { @@ -32,7 +46,7 @@ func ServeAsciiArt(reader io.Reader) { } go func(s net.Conn) { for { - n, err := s.Write([]byte(currentMessage)) + n, err := s.Write([]byte(*currentMessage)) if err != nil { log.Printf("Error while sending TCP data: %s", err) _ = s.Close() @@ -42,13 +56,28 @@ func ServeAsciiArt(reader io.Reader) { _ = s.Close() break } - time.Sleep(50 * time.Millisecond) + time.Sleep(time.Duration(Cfg.Delay) * time.Millisecond) } }(s) } }() - buff := make([]byte, 2048) + log.Println("Telnet server initialized") +} + +func asciiChar(pixel byte) string { + asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", ".", " "} + return asciiChars[(255-pixel)/23] +} + +// ServeAsciiArt starts a telnet server that send all packets as ASCII Art +func ServeAsciiArt(reader io.ReadCloser) { + if !Cfg.Enabled { + _ = reader.Close() + return + } + + buff := make([]byte, Cfg.Width*Cfg.Height) header := "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" for { n, _ := reader.Read(buff) @@ -56,13 +85,13 @@ func ServeAsciiArt(reader io.Reader) { break } imageStr := "" - for j := 0; j < 18; j++ { - for i := 0; i < 32; i++ { - pixel := buff[32*j+i] + for j := 0; j < Cfg.Height; j++ { + for i := 0; i < Cfg.Width; i++ { + pixel := buff[Cfg.Width*j+i] imageStr += asciiChar(pixel) + asciiChar(pixel) } imageStr += "\n" } - currentMessage = header + imageStr + *currentMessage = header + imageStr } } diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 3ba4cb5..5bc9cc5 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -3,6 +3,7 @@ package webrtc import ( "bufio" + "fmt" "gitlab.crans.org/nounous/ghostream/stream/telnet" "io" "log" @@ -53,7 +54,7 @@ func ingestFrom(inputChannel chan srt.Packet) { "-f", "rtp", "rtp://127.0.0.1:5004", "-vn", "-acodec", "libopus", "-cpu-used", "5", "-deadline", "1", "-qmin", "10", "-qmax", "42", "-error-resilient", "1", "-auto-alt-ref", "1", "-f", "rtp", "rtp://127.0.0.1:5005", - "-an", "-f", "rawvideo", "-vf", "scale=32x18", "-pix_fmt", "gray", "pipe:1") + "-an", "-f", "rawvideo", "-vf", fmt.Sprintf("scale=%dx%d", telnet.Cfg.Width, telnet.Cfg.Height), "-pix_fmt", "gray", "pipe:1") input, err := ffmpeg.StdinPipe() if err != nil { From ee769518543144eeacd9102f51e66cd9b7e3724a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 00:30:05 +0200 Subject: [PATCH 04/26] Don't export to ASCII art if the telnet packet is disabled --- stream/webrtc/ingest.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 5bc9cc5..bab9b34 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -47,14 +47,21 @@ func ingestFrom(inputChannel chan srt.Packet) { } }() - ffmpeg = exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error", "-re", "-i", "pipe:0", + ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-re", "-i", "pipe:0", "-an", "-vcodec", "libvpx", "-crf", "10", "-cpu-used", "5", "-b:v", "6000k", "-maxrate", "8000k", "-bufsize", "12000k", // TODO Change bitrate when changing quality "-qmin", "10", "-qmax", "42", "-threads", "4", "-deadline", "1", "-error-resilient", "1", "-auto-alt-ref", "1", "-f", "rtp", "rtp://127.0.0.1:5004", "-vn", "-acodec", "libopus", "-cpu-used", "5", "-deadline", "1", "-qmin", "10", "-qmax", "42", "-error-resilient", "1", "-auto-alt-ref", "1", - "-f", "rtp", "rtp://127.0.0.1:5005", - "-an", "-f", "rawvideo", "-vf", fmt.Sprintf("scale=%dx%d", telnet.Cfg.Width, telnet.Cfg.Height), "-pix_fmt", "gray", "pipe:1") + "-f", "rtp", "rtp://127.0.0.1:5005"} + + // Export stream to ascii art + if telnet.Cfg.Enabled { + ffmpegArgs = append(ffmpegArgs, + "-an", "-f", "rawvideo", "-vf", fmt.Sprintf("scale=%dx%d", telnet.Cfg.Width, telnet.Cfg.Height), "-pix_fmt", "gray", "pipe:1") + } + + ffmpeg = exec.Command("ffmpeg", ffmpegArgs...) input, err := ffmpeg.StdinPipe() if err != nil { @@ -65,10 +72,6 @@ func ingestFrom(inputChannel chan srt.Packet) { if err != nil { panic(err) } - output, err := ffmpeg.StdoutPipe() - if err != nil { - panic(err) - } if err := ffmpeg.Start(); err != nil { panic(err) @@ -106,8 +109,14 @@ func ingestFrom(inputChannel chan srt.Packet) { } }() - // Receive ascii - go telnet.ServeAsciiArt(output) + // Receive raw video output and convert it to ASCII art, then forward it TCP + if telnet.Cfg.Enabled { + output, err := ffmpeg.StdoutPipe() + if err != nil { + panic(err) + } + go telnet.ServeAsciiArt(output) + } // Receive audio go func() { From 95fcedf2fac5963a94ef412f7682ce9e91e4772f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 00:31:47 +0200 Subject: [PATCH 05/26] Comment telnet package --- stream/telnet/telnet.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 8ad53df..874fff1 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -9,6 +9,8 @@ import ( ) var ( + // TODO Config should not be exported + // Cfg contains the different options of the telnet package, see below Cfg *Options currentMessage *string ) @@ -22,6 +24,7 @@ type Options struct { Delay int } +// Serve starts the telnet server and listen to clients func Serve(config *Options) { Cfg = config @@ -70,7 +73,7 @@ func asciiChar(pixel byte) string { return asciiChars[(255-pixel)/23] } -// ServeAsciiArt starts a telnet server that send all packets as ASCII Art +// ServeAsciiArt send all packets received by ffmpeg as ASCII Art to telnet clients func ServeAsciiArt(reader io.ReadCloser) { if !Cfg.Enabled { _ = reader.Close() From 727865f4442358bb698b76046f275c8d8612cf08 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 00:52:08 +0200 Subject: [PATCH 06/26] Interact with telnet to select the stream id --- stream/telnet/telnet.go | 57 ++++++++++++++++++++++++++++++++++++----- stream/webrtc/ingest.go | 18 ++++++------- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 874fff1..b955f5c 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -5,6 +5,7 @@ import ( "io" "log" "net" + "strings" "time" ) @@ -12,7 +13,7 @@ var ( // TODO Config should not be exported // Cfg contains the different options of the telnet package, see below Cfg *Options - currentMessage *string + currentMessage map[string]string ) // Options holds telnet package configuration @@ -32,14 +33,14 @@ func Serve(config *Options) { return } + currentMessage = make(map[string]string) + listener, err := net.Listen("tcp", Cfg.ListenAddress) if err != nil { log.Printf("Error while listening to the address %s: %s", Cfg.ListenAddress, err) return } - currentMessage = new(string) - go func() { for { s, err := listener.Accept() @@ -47,9 +48,51 @@ func Serve(config *Options) { log.Printf("Error while accepting TCP socket: %s", s) continue } + go func(s net.Conn) { + streamID := "" + // Request for stream ID for { - n, err := s.Write([]byte(*currentMessage)) + _, _ = s.Write([]byte("[GHOSTREAM]\n")) + _, err = s.Write([]byte("Enter stream ID: ")) + if err != nil { + log.Println("Error while requesting stream ID to telnet client") + _ = s.Close() + return + } + buff := make([]byte, 255) + n, err := s.Read(buff) + if err != nil { + log.Println("Error while requesting stream ID to telnet client") + _ = s.Close() + return + } + + streamID = string(buff[:n]) + streamID = strings.Replace(streamID, "\r", "", -1) + streamID = strings.Replace(streamID, "\n", "", -1) + + if len(streamID) > 0 { + if strings.ToLower(streamID) == "exit" { + _, _ = s.Write([]byte("Goodbye!\n")) + _ = s.Close() + return + } + if _, ok := currentMessage[streamID]; !ok { + _, err = s.Write([]byte("Unknown stream ID.\n")) + if err != nil { + log.Println("Error while requesting stream ID to telnet client") + _ = s.Close() + return + } + continue + } + break + } + } + + for { + n, err := s.Write([]byte(currentMessage[streamID])) if err != nil { log.Printf("Error while sending TCP data: %s", err) _ = s.Close() @@ -70,11 +113,11 @@ func Serve(config *Options) { func asciiChar(pixel byte) string { asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", ".", " "} - return asciiChars[(255-pixel)/23] + return asciiChars[(255-pixel)/22] } // ServeAsciiArt send all packets received by ffmpeg as ASCII Art to telnet clients -func ServeAsciiArt(reader io.ReadCloser) { +func ServeAsciiArt(streamID string, reader io.ReadCloser) { if !Cfg.Enabled { _ = reader.Close() return @@ -95,6 +138,6 @@ func ServeAsciiArt(reader io.ReadCloser) { } imageStr += "\n" } - *currentMessage = header + imageStr + currentMessage[streamID] = header + imageStr } } diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index bab9b34..37c4654 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -73,6 +73,15 @@ func ingestFrom(inputChannel chan srt.Packet) { panic(err) } + // Receive raw video output and convert it to ASCII art, then forward it TCP + if telnet.Cfg.Enabled { + output, err := ffmpeg.StdoutPipe() + if err != nil { + panic(err) + } + go telnet.ServeAsciiArt(srtPacket.StreamName, output) + } + if err := ffmpeg.Start(); err != nil { panic(err) } @@ -109,15 +118,6 @@ func ingestFrom(inputChannel chan srt.Packet) { } }() - // Receive raw video output and convert it to ASCII art, then forward it TCP - if telnet.Cfg.Enabled { - output, err := ffmpeg.StdoutPipe() - if err != nil { - panic(err) - } - go telnet.ServeAsciiArt(output) - } - // Receive audio go func() { for { From 4db102c384f8fc43bec252d61d24377ecb553845 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 00:56:49 +0200 Subject: [PATCH 07/26] Ascii is a known keyword, must be in capital letters --- stream/telnet/telnet.go | 4 ++-- stream/webrtc/ingest.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index b955f5c..8f7c09f 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -116,8 +116,8 @@ func asciiChar(pixel byte) string { return asciiChars[(255-pixel)/22] } -// ServeAsciiArt send all packets received by ffmpeg as ASCII Art to telnet clients -func ServeAsciiArt(streamID string, reader io.ReadCloser) { +// StartASCIIArtStream send all packets received by ffmpeg as ASCII Art to telnet clients +func StartASCIIArtStream(streamID string, reader io.ReadCloser) { if !Cfg.Enabled { _ = reader.Close() return diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 37c4654..5f336d6 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -79,7 +79,7 @@ func ingestFrom(inputChannel chan srt.Packet) { if err != nil { panic(err) } - go telnet.ServeAsciiArt(srtPacket.StreamName, output) + go telnet.StartASCIIArtStream(srtPacket.StreamName, output) } if err := ffmpeg.Start(); err != nil { From 7325301574405f68649ce00bae9615e8c8286e6d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 01:41:17 +0200 Subject: [PATCH 08/26] Comment in the right order --- stream/telnet/telnet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 8f7c09f..55e7db6 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -10,8 +10,8 @@ import ( ) var ( - // TODO Config should not be exported // Cfg contains the different options of the telnet package, see below + // TODO Config should not be exported Cfg *Options currentMessage map[string]string ) From defba5256901360a00cf91fe20e678990556af6e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 09:38:44 +0200 Subject: [PATCH 09/26] If there is no forwarding, drop forwarding channels --- stream/forwarding/forwarding.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stream/forwarding/forwarding.go b/stream/forwarding/forwarding.go index c6fdf9b..e32316f 100644 --- a/stream/forwarding/forwarding.go +++ b/stream/forwarding/forwarding.go @@ -18,7 +18,9 @@ type Options map[string][]string func Serve(inputChannel chan srt.Packet, cfg Options) { if len(cfg) < 1 { // No forwarding, ignore - return + for { + <-inputChannel // Clear input channel + } } log.Printf("Stream forwarding initialized") From 32f877508d0c20cb306406644f4e488ede69a841 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 09:50:46 +0200 Subject: [PATCH 10/26] Separate the WebRTC stream subroutine in a dedicated subroutine --- stream/webrtc/ingest.go | 286 +++++++++++++++++++++------------------- 1 file changed, 152 insertions(+), 134 deletions(-) diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 5f336d6..4ba9170 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -15,156 +15,37 @@ import ( "gitlab.crans.org/nounous/ghostream/stream/srt" ) +var ( + ffmpeg = make(map[string]*exec.Cmd) + ffmpegInput = make(map[string]io.WriteCloser) +) + func ingestFrom(inputChannel chan srt.Packet) { // FIXME Clean code - var ffmpeg *exec.Cmd - var ffmpegInput io.WriteCloser for { var err error = nil srtPacket := <-inputChannel + log.Println(len(inputChannel)) switch srtPacket.PacketType { case "register": - log.Printf("WebRTC RegisterStream %s", srtPacket.StreamName) - - // Open a UDP Listener for RTP Packets on port 5004 - videoListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5004}) - if err != nil { - log.Printf("Faited to open UDP listener %s", err) - return - } - audioListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5005}) - if err != nil { - log.Printf("Faited to open UDP listener %s", err) - return - } - defer func() { - if err = videoListener.Close(); err != nil { - log.Printf("Faited to close UDP listener %s", err) - } - if err = audioListener.Close(); err != nil { - log.Printf("Faited to close UDP listener %s", err) - } - }() - - ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-re", "-i", "pipe:0", - "-an", "-vcodec", "libvpx", "-crf", "10", "-cpu-used", "5", "-b:v", "6000k", "-maxrate", "8000k", "-bufsize", "12000k", // TODO Change bitrate when changing quality - "-qmin", "10", "-qmax", "42", "-threads", "4", "-deadline", "1", "-error-resilient", "1", - "-auto-alt-ref", "1", - "-f", "rtp", "rtp://127.0.0.1:5004", - "-vn", "-acodec", "libopus", "-cpu-used", "5", "-deadline", "1", "-qmin", "10", "-qmax", "42", "-error-resilient", "1", "-auto-alt-ref", "1", - "-f", "rtp", "rtp://127.0.0.1:5005"} - - // Export stream to ascii art - if telnet.Cfg.Enabled { - ffmpegArgs = append(ffmpegArgs, - "-an", "-f", "rawvideo", "-vf", fmt.Sprintf("scale=%dx%d", telnet.Cfg.Width, telnet.Cfg.Height), "-pix_fmt", "gray", "pipe:1") - } - - ffmpeg = exec.Command("ffmpeg", ffmpegArgs...) - - input, err := ffmpeg.StdinPipe() - if err != nil { - panic(err) - } - ffmpegInput = input - errOutput, err := ffmpeg.StderrPipe() - if err != nil { - panic(err) - } - - // Receive raw video output and convert it to ASCII art, then forward it TCP - if telnet.Cfg.Enabled { - output, err := ffmpeg.StdoutPipe() - if err != nil { - panic(err) - } - go telnet.StartASCIIArtStream(srtPacket.StreamName, output) - } - - if err := ffmpeg.Start(); err != nil { - panic(err) - } - - // Receive video - go func() { - for { - inboundRTPPacket := make([]byte, 1500) // UDP MTU - n, _, err := videoListener.ReadFromUDP(inboundRTPPacket) - if err != nil { - log.Printf("Failed to read from UDP: %s", err) - continue - } - packet := &rtp.Packet{} - if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil { - log.Printf("Failed to unmarshal RTP srtPacket: %s", err) - continue - } - - if videoTracks[srtPacket.StreamName] == nil { - videoTracks[srtPacket.StreamName] = make([]*webrtc.Track, 0) - } - - // Write RTP srtPacket to all video tracks - // Adapt payload and SSRC to match destination - for _, videoTrack := range videoTracks[srtPacket.StreamName] { - packet.Header.PayloadType = videoTrack.PayloadType() - packet.Header.SSRC = videoTrack.SSRC() - if writeErr := videoTrack.WriteRTP(packet); writeErr != nil { - log.Printf("Failed to write to video track: %s", err) - continue - } - } - } - }() - - // Receive audio - go func() { - for { - inboundRTPPacket := make([]byte, 1500) // UDP MTU - n, _, err := audioListener.ReadFromUDP(inboundRTPPacket) - if err != nil { - log.Printf("Failed to read from UDP: %s", err) - continue - } - packet := &rtp.Packet{} - if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil { - log.Printf("Failed to unmarshal RTP srtPacket: %s", err) - continue - } - - if audioTracks[srtPacket.StreamName] == nil { - audioTracks[srtPacket.StreamName] = make([]*webrtc.Track, 0) - } - - // Write RTP srtPacket to all audio tracks - // Adapt payload and SSRC to match destination - for _, audioTrack := range audioTracks[srtPacket.StreamName] { - packet.Header.PayloadType = audioTrack.PayloadType() - packet.Header.SSRC = audioTrack.SSRC() - if writeErr := audioTrack.WriteRTP(packet); writeErr != nil { - log.Printf("Failed to write to audio track: %s", err) - continue - } - } - } - }() - - go func() { - scanner := bufio.NewScanner(errOutput) - for scanner.Scan() { - log.Printf("[WEBRTC FFMPEG %s] %s", "demo", scanner.Text()) - } - }() + go registerStream(&srtPacket) break case "sendData": + if _, ok := ffmpegInput[srtPacket.StreamName]; !ok { + break + } // FIXME send to stream srtPacket.StreamName - if _, err := ffmpegInput.Write(srtPacket.Data); err != nil { + if _, err := ffmpegInput[srtPacket.StreamName].Write(srtPacket.Data); err != nil { log.Printf("Failed to write data to ffmpeg input: %s", err) } break case "close": log.Printf("WebRTC CloseConnection %s", srtPacket.StreamName) + _ = ffmpeg[srtPacket.StreamName].Process.Kill() + _ = ffmpegInput[srtPacket.StreamName].Close() + delete(ffmpeg, srtPacket.StreamName) + delete(ffmpegInput, srtPacket.StreamName) break default: log.Println("Unknown SRT srtPacket type:", srtPacket.PacketType) @@ -175,3 +56,140 @@ func ingestFrom(inputChannel chan srt.Packet) { } } } + +func registerStream(srtPacket *srt.Packet) { + log.Printf("WebRTC RegisterStream %s", srtPacket.StreamName) + + // Open a UDP Listener for RTP Packets on port 5004 + videoListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5004}) + if err != nil { + log.Printf("Faited to open UDP listener %s", err) + return + } + audioListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5005}) + if err != nil { + log.Printf("Faited to open UDP listener %s", err) + return + } + // FIXME Close UDP listeners at the end of the stream, not the end of the routine + /* defer func() { + if err = videoListener.Close(); err != nil { + log.Printf("Faited to close UDP listener %s", err) + } + if err = audioListener.Close(); err != nil { + log.Printf("Faited to close UDP listener %s", err) + } + }() */ + + ffmpegArgs := []string{"-re", "-i", "pipe:0", + "-an", "-vcodec", "libvpx", "-crf", "10", "-cpu-used", "5", "-b:v", "6000k", "-maxrate", "8000k", "-bufsize", "12000k", // TODO Change bitrate when changing quality + "-qmin", "10", "-qmax", "42", "-threads", "4", "-deadline", "1", "-error-resilient", "1", + "-auto-alt-ref", "1", + "-f", "rtp", "rtp://127.0.0.1:5004", + "-vn", "-acodec", "libopus", "-cpu-used", "5", "-deadline", "1", "-qmin", "10", "-qmax", "42", "-error-resilient", "1", "-auto-alt-ref", "1", + "-f", "rtp", "rtp://127.0.0.1:5005"} + + // Export stream to ascii art + if telnet.Cfg.Enabled { + bitrate := fmt.Sprintf("%dk", telnet.Cfg.Width*telnet.Cfg.Height/telnet.Cfg.Delay) + ffmpegArgs = append(ffmpegArgs, + "-an", "-vf", fmt.Sprintf("scale=%dx%d", telnet.Cfg.Width, telnet.Cfg.Height), + "-b:v", bitrate, "-minrate", bitrate, "-maxrate", bitrate, "-bufsize", bitrate, "-q", "42", "-pix_fmt", "gray", "-f", "rawvideo", "pipe:1") + } + + ffmpeg[srtPacket.StreamName] = exec.Command("ffmpeg", ffmpegArgs...) + + input, err := ffmpeg[srtPacket.StreamName].StdinPipe() + if err != nil { + panic(err) + } + ffmpegInput[srtPacket.StreamName] = input + errOutput, err := ffmpeg[srtPacket.StreamName].StderrPipe() + if err != nil { + panic(err) + } + + // Receive raw video output and convert it to ASCII art, then forward it TCP + if telnet.Cfg.Enabled { + output, err := ffmpeg[srtPacket.StreamName].StdoutPipe() + if err != nil { + panic(err) + } + go telnet.StartASCIIArtStream(srtPacket.StreamName, output) + } + + if err := ffmpeg[srtPacket.StreamName].Start(); err != nil { + panic(err) + } + + // Receive video + go func() { + for { + inboundRTPPacket := make([]byte, 1500) // UDP MTU + n, _, err := videoListener.ReadFromUDP(inboundRTPPacket) + if err != nil { + log.Printf("Failed to read from UDP: %s", err) + continue + } + packet := &rtp.Packet{} + if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil { + log.Printf("Failed to unmarshal RTP srtPacket: %s", err) + continue + } + + if videoTracks[srtPacket.StreamName] == nil { + videoTracks[srtPacket.StreamName] = make([]*webrtc.Track, 0) + } + + // Write RTP srtPacket to all video tracks + // Adapt payload and SSRC to match destination + for _, videoTrack := range videoTracks[srtPacket.StreamName] { + packet.Header.PayloadType = videoTrack.PayloadType() + packet.Header.SSRC = videoTrack.SSRC() + if writeErr := videoTrack.WriteRTP(packet); writeErr != nil { + log.Printf("Failed to write to video track: %s", err) + continue + } + } + } + }() + + // Receive audio + go func() { + for { + inboundRTPPacket := make([]byte, 1500) // UDP MTU + n, _, err := audioListener.ReadFromUDP(inboundRTPPacket) + if err != nil { + log.Printf("Failed to read from UDP: %s", err) + continue + } + packet := &rtp.Packet{} + if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil { + log.Printf("Failed to unmarshal RTP srtPacket: %s", err) + continue + } + + if audioTracks[srtPacket.StreamName] == nil { + audioTracks[srtPacket.StreamName] = make([]*webrtc.Track, 0) + } + + // Write RTP srtPacket to all audio tracks + // Adapt payload and SSRC to match destination + for _, audioTrack := range audioTracks[srtPacket.StreamName] { + packet.Header.PayloadType = audioTrack.PayloadType() + packet.Header.SSRC = audioTrack.SSRC() + if writeErr := audioTrack.WriteRTP(packet); writeErr != nil { + log.Printf("Failed to write to audio track: %s", err) + continue + } + } + } + }() + + go func() { + scanner := bufio.NewScanner(errOutput) + for scanner.Scan() { + log.Printf("[WEBRTC FFMPEG %s] %s", "demo", scanner.Text()) + } + }() +} From 3ce82c5d6119d066c8cea39d9d902adccf98305c Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 10:10:25 +0200 Subject: [PATCH 11/26] Allocate memory for UDP buffers only once --- stream/webrtc/ingest.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 4ba9170..51e0112 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -4,15 +4,14 @@ package webrtc import ( "bufio" "fmt" + "github.com/pion/rtp" + "github.com/pion/webrtc/v3" + "gitlab.crans.org/nounous/ghostream/stream/srt" "gitlab.crans.org/nounous/ghostream/stream/telnet" "io" "log" "net" "os/exec" - - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - "gitlab.crans.org/nounous/ghostream/stream/srt" ) var ( @@ -124,8 +123,8 @@ func registerStream(srtPacket *srt.Packet) { // Receive video go func() { + inboundRTPPacket := make([]byte, 1500) // UDP MTU for { - inboundRTPPacket := make([]byte, 1500) // UDP MTU n, _, err := videoListener.ReadFromUDP(inboundRTPPacket) if err != nil { log.Printf("Failed to read from UDP: %s", err) @@ -156,8 +155,8 @@ func registerStream(srtPacket *srt.Packet) { // Receive audio go func() { + inboundRTPPacket := make([]byte, 1500) // UDP MTU for { - inboundRTPPacket := make([]byte, 1500) // UDP MTU n, _, err := audioListener.ReadFromUDP(inboundRTPPacket) if err != nil { log.Printf("Failed to read from UDP: %s", err) From 770862cb7d3408aa9ce0007a505f788d2554e616 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 10:46:04 +0200 Subject: [PATCH 12/26] Don't use -re ffmpeg option: the video speed is already cadenced by the streamer. Fix #16 --- stream/webrtc/ingest.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stream/webrtc/ingest.go b/stream/webrtc/ingest.go index 51e0112..0c4f9bb 100644 --- a/stream/webrtc/ingest.go +++ b/stream/webrtc/ingest.go @@ -25,7 +25,6 @@ func ingestFrom(inputChannel chan srt.Packet) { for { var err error = nil srtPacket := <-inputChannel - log.Println(len(inputChannel)) switch srtPacket.PacketType { case "register": go registerStream(&srtPacket) @@ -80,7 +79,7 @@ func registerStream(srtPacket *srt.Packet) { } }() */ - ffmpegArgs := []string{"-re", "-i", "pipe:0", + ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0", "-an", "-vcodec", "libvpx", "-crf", "10", "-cpu-used", "5", "-b:v", "6000k", "-maxrate", "8000k", "-bufsize", "12000k", // TODO Change bitrate when changing quality "-qmin", "10", "-qmax", "42", "-threads", "4", "-deadline", "1", "-error-resilient", "1", "-auto-alt-ref", "1", From 6d9fe4a02801cdfbb20ef30007e881b10ef59cfa Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 11:17:35 +0200 Subject: [PATCH 13/26] Use string pointers for the telnet output to avoid concurrency map read/write --- stream/telnet/telnet.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 55e7db6..7c6f832 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -13,7 +13,7 @@ var ( // Cfg contains the different options of the telnet package, see below // TODO Config should not be exported Cfg *Options - currentMessage map[string]string + currentMessage map[string]*string ) // Options holds telnet package configuration @@ -33,7 +33,7 @@ func Serve(config *Options) { return } - currentMessage = make(map[string]string) + currentMessage = make(map[string]*string) listener, err := net.Listen("tcp", Cfg.ListenAddress) if err != nil { @@ -92,7 +92,7 @@ func Serve(config *Options) { } for { - n, err := s.Write([]byte(currentMessage[streamID])) + n, err := s.Write([]byte(*currentMessage[streamID])) if err != nil { log.Printf("Error while sending TCP data: %s", err) _ = s.Close() @@ -123,6 +123,7 @@ func StartASCIIArtStream(streamID string, reader io.ReadCloser) { return } + currentMessage[streamID] = new(string) buff := make([]byte, Cfg.Width*Cfg.Height) header := "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" for { @@ -138,6 +139,6 @@ func StartASCIIArtStream(streamID string, reader io.ReadCloser) { } imageStr += "\n" } - currentMessage[streamID] = header + imageStr + *(currentMessage[streamID]) = header + imageStr } } From 0055b739179443f1d73a80b4d46dd7c04332c7bf Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 11:26:50 +0200 Subject: [PATCH 14/26] Expose port 23 for telnet inputs --- Dockerfile | 2 +- docs/ghostream.example.yml | 2 +- internal/config/config.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1a4affa..77a8a37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,5 +13,5 @@ RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/community/ COPY --from=build_base /code/out/ghostream /app/ghostream WORKDIR /app # 9710 for SRT, 8080 for Web, 2112 for monitoring and 10000-10005 (UDP) for WebRTC -EXPOSE 9710/udp 8080 2112 10000-10005/udp +EXPOSE 23 9710/udp 8080 2112 10000-10005/udp CMD ["/app/ghostream"] diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index 3b7d628..6ad859d 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -75,7 +75,7 @@ telnet: # You must enable it to use it. #enable: false - #listenAddress: :4242 + #listenAddress: :23 # Size is in characters. It is recommended to keep a 16x9 format. #width: 80 diff --git a/internal/config/config.go b/internal/config/config.go index eab8b79..5a59f8f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,7 +60,7 @@ func New() *Config { }, Telnet: telnet.Options{ Enabled: false, - ListenAddress: ":4242", + ListenAddress: ":23", Width: 80, Height: 45, Delay: 50, From 88c4a037cb13897297117c61de1c44139afcecae Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 11:28:29 +0200 Subject: [PATCH 15/26] Add 3 seconds delay before accepting telnet inputs to avoid bruteforce attacks --- stream/telnet/telnet.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 7c6f832..22034ef 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -68,6 +68,9 @@ func Serve(config *Options) { return } + // Avoid bruteforce + time.Sleep(3 * time.Second) + streamID = string(buff[:n]) streamID = strings.Replace(streamID, "\r", "", -1) streamID = strings.Replace(streamID, "\n", "", -1) From 51d38f6fec21f9969488ea1c48306df3c355823a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 11:37:59 +0200 Subject: [PATCH 16/26] Store the clients that are connected to a telnet shell in the connected viewers stats --- stream/telnet/telnet.go | 14 ++++++++++++++ web/handler.go | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 22034ef..83ecc7d 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -14,6 +14,7 @@ var ( // TODO Config should not be exported Cfg *Options currentMessage map[string]*string + clientCount map[string]int ) // Options holds telnet package configuration @@ -34,6 +35,7 @@ func Serve(config *Options) { } currentMessage = make(map[string]*string) + clientCount = make(map[string]int) listener, err := net.Listen("tcp", Cfg.ListenAddress) if err != nil { @@ -94,15 +96,19 @@ func Serve(config *Options) { } } + clientCount[streamID]++ + for { n, err := s.Write([]byte(*currentMessage[streamID])) if err != nil { log.Printf("Error while sending TCP data: %s", err) _ = s.Close() + clientCount[streamID]-- break } if n == 0 { _ = s.Close() + clientCount[streamID]-- break } time.Sleep(time.Duration(Cfg.Delay) * time.Millisecond) @@ -114,6 +120,14 @@ func Serve(config *Options) { log.Println("Telnet server initialized") } +// GetNumberConnectedSessions returns the numbers of clients that are viewing the stream through a telnet shell +func GetNumberConnectedSessions(streamID string) int { + if !Cfg.Enabled { + return 0 + } + return clientCount[streamID] +} + func asciiChar(pixel byte) string { asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", ".", " "} return asciiChars[(255-pixel)/22] diff --git a/web/handler.go b/web/handler.go index 197613e..b2e50ff 100644 --- a/web/handler.go +++ b/web/handler.go @@ -4,6 +4,7 @@ package web import ( "bytes" "encoding/json" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "html/template" "log" "net" @@ -144,7 +145,9 @@ func statisticsHandler(w http.ResponseWriter, r *http.Request) { enc := json.NewEncoder(w) err := enc.Encode(struct { ConnectedViewers int - }{webrtc.GetNumberConnectedSessions(streamID) + srt.GetNumberConnectedSessions(streamID)}) + }{webrtc.GetNumberConnectedSessions(streamID) + + srt.GetNumberConnectedSessions(streamID) + + telnet.GetNumberConnectedSessions(streamID)}) if err != nil { http.Error(w, "Failed to generate JSON.", http.StatusInternalServerError) log.Printf("Failed to generate JSON: %s", err) From 3b8c149e3859e3e888a4583ebdce9a4b94ab1562 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 11:38:22 +0200 Subject: [PATCH 17/26] Port 8023 is better, it is non protected. Users are free to bind another port --- Dockerfile | 4 ++-- docs/ghostream.example.yml | 2 +- internal/config/config.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77a8a37..6f0e5fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,6 @@ FROM alpine:3.12 RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/community/ ffmpeg libsrt COPY --from=build_base /code/out/ghostream /app/ghostream WORKDIR /app -# 9710 for SRT, 8080 for Web, 2112 for monitoring and 10000-10005 (UDP) for WebRTC -EXPOSE 23 9710/udp 8080 2112 10000-10005/udp +# 9710 for SRT, 8080 for Web, 2112 for monitoring, 10000-10005 (UDP) for WebRTC and 23 for telnet +EXPOSE 9710/udp 8080 2112 10000-10005/udp 8023 CMD ["/app/ghostream"] diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index 6ad859d..98b8554 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -75,7 +75,7 @@ telnet: # You must enable it to use it. #enable: false - #listenAddress: :23 + #listenAddress: :8023 # Size is in characters. It is recommended to keep a 16x9 format. #width: 80 diff --git a/internal/config/config.go b/internal/config/config.go index 5a59f8f..a6bd1ce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,7 +60,7 @@ func New() *Config { }, Telnet: telnet.Options{ Enabled: false, - ListenAddress: ":23", + ListenAddress: ":8023", Width: 80, Height: 45, Delay: 50, From 4546f3b8fb07bc724add364fdad8d74fe798587d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 17:12:19 +0200 Subject: [PATCH 18/26] Map domain to streamid instead of considering that the domain is the streamid: no need to pass a YAML key that contains dots --- docs/ghostream.example.yml | 10 ++++++---- internal/config/config.go | 2 +- web/handler.go | 40 +++++++++++++++++++------------------- web/template/_base.html | 2 +- web/web.go | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index 98b8554..4856b96 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -73,7 +73,7 @@ srt: telnet: # By default, this easter egg is disabled. # You must enable it to use it. - #enable: false + #enabled: false #listenAddress: :8023 @@ -115,11 +115,13 @@ web: # #name: Ghostream - # Use the domain name as the stream name - # e.g., on http://example.com:8080/ the stream served will be "example.com" + # Use the domain name as the stream name for some hosts + # e.g., on http://stream.example.com:8080/, if the domain stream.example.com is mapped to "example", + # the stream served will be "example". # This implies that your domain will be able to serve only one stream. # - #oneStreamPerDomain: false + #mapDomainToStream: + # stream.example.com: example # Stream player poster # Shown when stream is loading or inactive. diff --git a/internal/config/config.go b/internal/config/config.go index a6bd1ce..1ff252e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -71,7 +71,7 @@ func New() *Config { Hostname: "localhost", ListenAddress: ":8080", Name: "Ghostream", - OneStreamPerDomain: false, + MapDomainToStream: make(map[string]string), PlayerPoster: "/static/img/no_stream.svg", ViewersCounterRefreshPeriod: 20000, }, diff --git a/web/handler.go b/web/handler.go index b2e50ff..9549032 100644 --- a/web/handler.go +++ b/web/handler.go @@ -24,17 +24,17 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) { // Get stream ID from URL, or from domain name path := r.URL.Path[1:] - if cfg.OneStreamPerDomain { - host := r.Host - if strings.Contains(host, ":") { - realHost, _, err := net.SplitHostPort(r.Host) - if err != nil { - log.Printf("Failed to split host and port from %s", r.Host) - return - } - host = realHost + host := r.Host + if strings.Contains(host, ":") { + realHost, _, err := net.SplitHostPort(r.Host) + if err != nil { + log.Printf("Failed to split host and port from %s", r.Host) + return } - path = host + host = realHost + } + if streamID, ok := cfg.MapDomainToStream[host]; ok { + path = streamID } // Decode client description @@ -73,20 +73,20 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) { func viewerGetHandler(w http.ResponseWriter, r *http.Request) { // Get stream ID from URL, or from domain name path := r.URL.Path[1:] - if cfg.OneStreamPerDomain { - host := r.Host - if strings.Contains(host, ":") { - realHost, _, err := net.SplitHostPort(r.Host) - if err != nil { - log.Printf("Failed to split host and port from %s", r.Host) - return - } - host = realHost + host := r.Host + if strings.Contains(host, ":") { + realHost, _, err := net.SplitHostPort(r.Host) + if err != nil { + log.Printf("Failed to split host and port from %s", r.Host) + return } + host = realHost + } + if streamID, ok := cfg.MapDomainToStream[host]; ok { if path == "about" { path = "" } else { - path = host + path = streamID } } diff --git a/web/template/_base.html b/web/template/_base.html index 78fc1be..35e4de6 100644 --- a/web/template/_base.html +++ b/web/template/_base.html @@ -4,7 +4,7 @@ - {{if .Path}}{{if not .Cfg.OneStreamPerDomain}}{{.Path}} - {{end}}{{end}}{{.Cfg.Name}} + {{if .Path}}{{.Path}} - {{end}}{{.Cfg.Name}} {{if .Cfg.CustomCSS}}{{end}} diff --git a/web/web.go b/web/web.go index ef25844..5f9161c 100644 --- a/web/web.go +++ b/web/web.go @@ -22,7 +22,7 @@ type Options struct { Hostname string ListenAddress string Name string - OneStreamPerDomain bool + MapDomainToStream map[string]string PlayerPoster string SRTServerPort string STUNServers []string From de2ac302926e9f1757dbe9b8f8efbab0e1501bf6 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 17:18:44 +0200 Subject: [PATCH 19/26] Replace dots by underscores in MapDomainToStream configuration --- docs/ghostream.example.yml | 3 ++- web/handler.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index 4856b96..f583472 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -119,9 +119,10 @@ web: # e.g., on http://stream.example.com:8080/, if the domain stream.example.com is mapped to "example", # the stream served will be "example". # This implies that your domain will be able to serve only one stream. + # Dots in the domain name must be remplaced by underscores to avoid yaml issues. # #mapDomainToStream: - # stream.example.com: example + # stream_example_com: example # Stream player poster # Shown when stream is loading or inactive. diff --git a/web/handler.go b/web/handler.go index 9549032..cf3a04d 100644 --- a/web/handler.go +++ b/web/handler.go @@ -32,6 +32,7 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) { return } host = realHost + host = strings.Replace(host, ".", "_", -1) } if streamID, ok := cfg.MapDomainToStream[host]; ok { path = streamID @@ -81,6 +82,7 @@ func viewerGetHandler(w http.ResponseWriter, r *http.Request) { return } host = realHost + host = strings.Replace(host, ".", "_", -1) } if streamID, ok := cfg.MapDomainToStream[host]; ok { if path == "about" { From a2efa1126f782b32db2938627b679dbddad57aa9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 17:25:00 +0200 Subject: [PATCH 20/26] Underscores are ignored by YAML, uses dashes --- docs/ghostream.example.yml | 4 ++-- web/handler.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index f583472..c739121 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -119,10 +119,10 @@ web: # e.g., on http://stream.example.com:8080/, if the domain stream.example.com is mapped to "example", # the stream served will be "example". # This implies that your domain will be able to serve only one stream. - # Dots in the domain name must be remplaced by underscores to avoid yaml issues. + # Dots in the domain name must be remplaced by dashes to avoid yaml issues. # #mapDomainToStream: - # stream_example_com: example + # stream-example-com: example # Stream player poster # Shown when stream is loading or inactive. diff --git a/web/handler.go b/web/handler.go index cf3a04d..d390db6 100644 --- a/web/handler.go +++ b/web/handler.go @@ -32,8 +32,8 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) { return } host = realHost - host = strings.Replace(host, ".", "_", -1) } + host = strings.Replace(host, ".", "-", -1) if streamID, ok := cfg.MapDomainToStream[host]; ok { path = streamID } @@ -82,8 +82,8 @@ func viewerGetHandler(w http.ResponseWriter, r *http.Request) { return } host = realHost - host = strings.Replace(host, ".", "_", -1) } + host = strings.Replace(host, ".", "-", -1) if streamID, ok := cfg.MapDomainToStream[host]; ok { if path == "about" { path = "" From e154fe1a1ead232be183e5983477110d592ece8f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 17:43:53 +0200 Subject: [PATCH 21/26] Stream ID was broken in the current viewers stats --- web/static/js/viewersCounter.js | 4 ++-- web/template/player.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/static/js/viewersCounter.js b/web/static/js/viewersCounter.js index 2b0ba2b..29d5015 100644 --- a/web/static/js/viewersCounter.js +++ b/web/static/js/viewersCounter.js @@ -1,7 +1,7 @@ // Refresh viewer count by pulling metric from server -function refreshViewersCounter(period) { +function refreshViewersCounter(streamID, period) { // Distinguish oneDomainPerStream mode - fetch("/_stats/" + (location.pathname === "/" ? location.host : location.pathname.substring(1))) + fetch("/_stats/" + streamID) .then(response => response.json()) .then((data) => document.getElementById("connected-people").innerText = data.ConnectedViewers) .catch(console.log) diff --git a/web/template/player.html b/web/template/player.html index ae7d284..4f753fa 100644 --- a/web/template/player.html +++ b/web/template/player.html @@ -50,7 +50,7 @@ // Wait a bit before pulling viewers counter for the first time setTimeout(() => { - refreshViewersCounter({{.Cfg.ViewersCounterRefreshPeriod}}) + refreshViewersCounter({{.Path}}, {{.Cfg.ViewersCounterRefreshPeriod}}) }, 1000) {{end}} \ No newline at end of file From a6fd1344bcaa2dccbff9988e0e3b464c8ecc41da Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 17:57:38 +0200 Subject: [PATCH 22/26] Avoid to DDOS the server, querying infinite time per second a blank page is maybe too much, useless and dangerous --- web/static/js/viewersCounter.js | 2 +- web/template/player.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/static/js/viewersCounter.js b/web/static/js/viewersCounter.js index 29d5015..7298f6e 100644 --- a/web/static/js/viewersCounter.js +++ b/web/static/js/viewersCounter.js @@ -7,6 +7,6 @@ function refreshViewersCounter(streamID, period) { .catch(console.log) setTimeout(() => { - refreshViewersCounter(period) + refreshViewersCounter(streamID, period) }, period) } diff --git a/web/template/player.html b/web/template/player.html index 4f753fa..46749bf 100644 --- a/web/template/player.html +++ b/web/template/player.html @@ -50,7 +50,7 @@ // Wait a bit before pulling viewers counter for the first time setTimeout(() => { - refreshViewersCounter({{.Path}}, {{.Cfg.ViewersCounterRefreshPeriod}}) + refreshViewersCounter("{{.Path}}", {{.Cfg.ViewersCounterRefreshPeriod}}) }, 1000) {{end}} \ No newline at end of file From 0dc89b57e178e42fbeec5476712d922476bb25fa Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 18:01:20 +0200 Subject: [PATCH 23/26] Order exposed ports --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f0e5fb..c52587a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,6 @@ FROM alpine:3.12 RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/community/ ffmpeg libsrt COPY --from=build_base /code/out/ghostream /app/ghostream WORKDIR /app -# 9710 for SRT, 8080 for Web, 2112 for monitoring, 10000-10005 (UDP) for WebRTC and 23 for telnet -EXPOSE 9710/udp 8080 2112 10000-10005/udp 8023 +# 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-10005 (UDP) for WebRTC +EXPOSE 2112 8023 8080 9710/udp 10000-10005/udp CMD ["/app/ghostream"] From 771a7c1c1bf5221cb38f17e657695407951d7045 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 18:03:14 +0200 Subject: [PATCH 24/26] Better comments in example configuration file --- docs/ghostream.example.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/ghostream.example.yml b/docs/ghostream.example.yml index c739121..86511b7 100644 --- a/docs/ghostream.example.yml +++ b/docs/ghostream.example.yml @@ -73,15 +73,19 @@ srt: telnet: # By default, this easter egg is disabled. # You must enable it to use it. + # #enabled: false #listenAddress: :8023 # Size is in characters. It is recommended to keep a 16x9 format. + # #width: 80 #height: 45 - # Time in milliseconds that we should sleep between two images. By default, 20 FPS. Displaying text takes time... + # Time in milliseconds that we should sleep between two images. + # By default, 20 FPS. Displaying text takes time... + # #delay: 50 From 084ea676be2201c9919ecd02395c6dd4bf306a0b Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 18:04:00 +0200 Subject: [PATCH 25/26] Sort imports --- internal/config/config.go | 2 +- main.go | 2 +- web/handler.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 1ff252e..168085f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,7 +3,6 @@ package config import ( "bytes" - "gitlab.crans.org/nounous/ghostream/stream/telnet" "log" "net" "strings" @@ -15,6 +14,7 @@ import ( "gitlab.crans.org/nounous/ghostream/internal/monitoring" "gitlab.crans.org/nounous/ghostream/stream/forwarding" "gitlab.crans.org/nounous/ghostream/stream/srt" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "gitlab.crans.org/nounous/ghostream/stream/webrtc" "gitlab.crans.org/nounous/ghostream/web" "gopkg.in/yaml.v2" diff --git a/main.go b/main.go index 36815b2..82d78b8 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ package main import ( - "gitlab.crans.org/nounous/ghostream/stream/telnet" "log" "gitlab.crans.org/nounous/ghostream/auth" @@ -13,6 +12,7 @@ import ( "gitlab.crans.org/nounous/ghostream/internal/monitoring" "gitlab.crans.org/nounous/ghostream/stream/forwarding" "gitlab.crans.org/nounous/ghostream/stream/srt" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "gitlab.crans.org/nounous/ghostream/stream/webrtc" "gitlab.crans.org/nounous/ghostream/web" ) diff --git a/web/handler.go b/web/handler.go index d390db6..2ba4bd9 100644 --- a/web/handler.go +++ b/web/handler.go @@ -4,7 +4,6 @@ package web import ( "bytes" "encoding/json" - "gitlab.crans.org/nounous/ghostream/stream/telnet" "html/template" "log" "net" @@ -14,6 +13,7 @@ import ( "github.com/markbates/pkger" "gitlab.crans.org/nounous/ghostream/internal/monitoring" "gitlab.crans.org/nounous/ghostream/stream/srt" + "gitlab.crans.org/nounous/ghostream/stream/telnet" "gitlab.crans.org/nounous/ghostream/stream/webrtc" ) From 029633d215dc575e823adb8e3a72cd145b725e33 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 13 Oct 2020 18:40:12 +0200 Subject: [PATCH 26/26] Don't fail tests if the telnet module is not loaded --- stream/telnet/telnet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream/telnet/telnet.go b/stream/telnet/telnet.go index 83ecc7d..d14024f 100644 --- a/stream/telnet/telnet.go +++ b/stream/telnet/telnet.go @@ -122,7 +122,7 @@ func Serve(config *Options) { // GetNumberConnectedSessions returns the numbers of clients that are viewing the stream through a telnet shell func GetNumberConnectedSessions(streamID string) int { - if !Cfg.Enabled { + if Cfg == nil || !Cfg.Enabled { return 0 } return clientCount[streamID]