mirror of
https://gitlab.crans.org/nounous/ghostream.git
synced 2024-12-22 15:02:19 +00:00
Merge branch 'asciiart' into 'dev'
Asciiart and performance Closes #16 See merge request nounous/ghostream!4
This commit is contained in:
commit
d6750f219a
@ -12,6 +12,6 @@ FROM alpine:3.12
|
|||||||
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/community/ ffmpeg libsrt
|
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
|
COPY --from=build_base /code/out/ghostream /app/ghostream
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# 9710 for SRT, 8080 for Web, 2112 for monitoring and 10000-10005 (UDP) for WebRTC
|
# 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-10005 (UDP) for WebRTC
|
||||||
EXPOSE 9710/udp 8080 2112 10000-10005/udp
|
EXPOSE 2112 8023 8080 9710/udp 10000-10005/udp
|
||||||
CMD ["/app/ghostream"]
|
CMD ["/app/ghostream"]
|
||||||
|
@ -73,6 +73,27 @@ srt:
|
|||||||
# Max number of active SRT connections
|
# Max number of active SRT connections
|
||||||
#maxClients: 64
|
#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.
|
||||||
|
#
|
||||||
|
#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...
|
||||||
|
#
|
||||||
|
#delay: 50
|
||||||
|
|
||||||
|
|
||||||
## Web server ##
|
## Web server ##
|
||||||
# The web server serves a WebRTC player.
|
# The web server serves a WebRTC player.
|
||||||
web:
|
web:
|
||||||
@ -103,11 +124,14 @@ web:
|
|||||||
#
|
#
|
||||||
#name: Ghostream
|
#name: Ghostream
|
||||||
|
|
||||||
# Use the domain name as the stream name
|
# Use the domain name as the stream name for some hosts
|
||||||
# e.g., on http://example.com:8080/ the stream served will be "example.com"
|
# 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.
|
# This implies that your domain will be able to serve only one stream.
|
||||||
|
# Dots in the domain name must be remplaced by dashes to avoid yaml issues.
|
||||||
#
|
#
|
||||||
#oneStreamPerDomain: false
|
#mapDomainToStream:
|
||||||
|
# stream-example-com: example
|
||||||
|
|
||||||
# Stream player poster
|
# Stream player poster
|
||||||
# Shown when stream is loading or inactive.
|
# Shown when stream is loading or inactive.
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/forwarding"
|
"gitlab.crans.org/nounous/ghostream/stream/forwarding"
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/srt"
|
"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/stream/webrtc"
|
||||||
"gitlab.crans.org/nounous/ghostream/web"
|
"gitlab.crans.org/nounous/ghostream/web"
|
||||||
)
|
)
|
||||||
@ -21,6 +22,7 @@ type Config struct {
|
|||||||
Forwarding forwarding.Options
|
Forwarding forwarding.Options
|
||||||
Monitoring monitoring.Options
|
Monitoring monitoring.Options
|
||||||
Srt srt.Options
|
Srt srt.Options
|
||||||
|
Telnet telnet.Options
|
||||||
Web web.Options
|
Web web.Options
|
||||||
WebRTC webrtc.Options
|
WebRTC webrtc.Options
|
||||||
}
|
}
|
||||||
@ -49,13 +51,20 @@ func New() *Config {
|
|||||||
ListenAddress: ":9710",
|
ListenAddress: ":9710",
|
||||||
MaxClients: 64,
|
MaxClients: 64,
|
||||||
},
|
},
|
||||||
|
Telnet: telnet.Options{
|
||||||
|
Enabled: false,
|
||||||
|
ListenAddress: ":8023",
|
||||||
|
Width: 80,
|
||||||
|
Height: 45,
|
||||||
|
Delay: 50,
|
||||||
|
},
|
||||||
Web: web.Options{
|
Web: web.Options{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Favicon: "/static/img/favicon.svg",
|
Favicon: "/static/img/favicon.svg",
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
ListenAddress: ":8080",
|
ListenAddress: ":8080",
|
||||||
Name: "Ghostream",
|
Name: "Ghostream",
|
||||||
OneStreamPerDomain: false,
|
MapDomainToStream: make(map[string]string),
|
||||||
PlayerPoster: "/static/img/no_stream.svg",
|
PlayerPoster: "/static/img/no_stream.svg",
|
||||||
ViewersCounterRefreshPeriod: 20000,
|
ViewersCounterRefreshPeriod: 20000,
|
||||||
},
|
},
|
||||||
|
2
main.go
2
main.go
@ -12,6 +12,7 @@ import (
|
|||||||
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/forwarding"
|
"gitlab.crans.org/nounous/ghostream/stream/forwarding"
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/srt"
|
"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/stream/webrtc"
|
||||||
"gitlab.crans.org/nounous/ghostream/web"
|
"gitlab.crans.org/nounous/ghostream/web"
|
||||||
)
|
)
|
||||||
@ -50,6 +51,7 @@ func main() {
|
|||||||
go forwarding.Serve(forwardingChannel, cfg.Forwarding)
|
go forwarding.Serve(forwardingChannel, cfg.Forwarding)
|
||||||
go monitoring.Serve(&cfg.Monitoring)
|
go monitoring.Serve(&cfg.Monitoring)
|
||||||
go srt.Serve(&cfg.Srt, authBackend, forwardingChannel, webrtcChannel)
|
go srt.Serve(&cfg.Srt, authBackend, forwardingChannel, webrtcChannel)
|
||||||
|
go telnet.Serve(&cfg.Telnet)
|
||||||
go web.Serve(remoteSdpChan, localSdpChan, &cfg.Web)
|
go web.Serve(remoteSdpChan, localSdpChan, &cfg.Web)
|
||||||
go webrtc.Serve(remoteSdpChan, localSdpChan, webrtcChannel, &cfg.WebRTC)
|
go webrtc.Serve(remoteSdpChan, localSdpChan, webrtcChannel, &cfg.WebRTC)
|
||||||
|
|
||||||
|
@ -18,7 +18,9 @@ type Options map[string][]string
|
|||||||
func Serve(inputChannel chan srt.Packet, cfg Options) {
|
func Serve(inputChannel chan srt.Packet, cfg Options) {
|
||||||
if len(cfg) < 1 {
|
if len(cfg) < 1 {
|
||||||
// No forwarding, ignore
|
// No forwarding, ignore
|
||||||
return
|
for {
|
||||||
|
<-inputChannel // Clear input channel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Stream forwarding initialized")
|
log.Printf("Stream forwarding initialized")
|
||||||
|
161
stream/telnet/telnet.go
Normal file
161
stream/telnet/telnet.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Package telnet provides some fancy tools, like an ASCII-art stream.
|
||||||
|
package telnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Cfg contains the different options of the telnet package, see below
|
||||||
|
// TODO Config should not be exported
|
||||||
|
Cfg *Options
|
||||||
|
currentMessage map[string]*string
|
||||||
|
clientCount map[string]int
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options holds telnet package configuration
|
||||||
|
type Options struct {
|
||||||
|
Enabled bool
|
||||||
|
ListenAddress string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Delay int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve starts the telnet server and listen to clients
|
||||||
|
func Serve(config *Options) {
|
||||||
|
Cfg = config
|
||||||
|
|
||||||
|
if !Cfg.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMessage = make(map[string]*string)
|
||||||
|
clientCount = make(map[string]int)
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", Cfg.ListenAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error while listening to the address %s: %s", Cfg.ListenAddress, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
streamID := ""
|
||||||
|
// Request for stream ID
|
||||||
|
for {
|
||||||
|
_, _ = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid bruteforce
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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 == nil || !Cfg.Enabled {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return clientCount[streamID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func asciiChar(pixel byte) string {
|
||||||
|
asciiChars := []string{"@", "#", "$", "%", "?", "*", "+", ";", ":", ",", ".", " "}
|
||||||
|
return asciiChars[(255-pixel)/22]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
n, _ := reader.Read(buff)
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
imageStr := ""
|
||||||
|
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[streamID]) = header + imageStr
|
||||||
|
}
|
||||||
|
}
|
@ -3,149 +3,47 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"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"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/pion/rtp"
|
var (
|
||||||
"github.com/pion/webrtc/v3"
|
ffmpeg = make(map[string]*exec.Cmd)
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/srt"
|
ffmpegInput = make(map[string]io.WriteCloser)
|
||||||
)
|
)
|
||||||
|
|
||||||
func ingestFrom(inputChannel chan srt.Packet) {
|
func ingestFrom(inputChannel chan srt.Packet) {
|
||||||
// FIXME Clean code
|
// FIXME Clean code
|
||||||
var ffmpeg *exec.Cmd
|
|
||||||
var ffmpegInput io.WriteCloser
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
srtPacket := <-inputChannel
|
srtPacket := <-inputChannel
|
||||||
switch srtPacket.PacketType {
|
switch srtPacket.PacketType {
|
||||||
case "register":
|
case "register":
|
||||||
log.Printf("WebRTC RegisterStream %s", srtPacket.StreamName)
|
go registerStream(&srtPacket)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ffmpeg = exec.Command("ffmpeg", "-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")
|
|
||||||
|
|
||||||
input, err := ffmpeg.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
ffmpegInput = input
|
|
||||||
errOutput, err := ffmpeg.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
break
|
break
|
||||||
case "sendData":
|
case "sendData":
|
||||||
|
if _, ok := ffmpegInput[srtPacket.StreamName]; !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
// FIXME send to stream srtPacket.StreamName
|
// 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)
|
log.Printf("Failed to write data to ffmpeg input: %s", err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "close":
|
case "close":
|
||||||
log.Printf("WebRTC CloseConnection %s", srtPacket.StreamName)
|
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
|
break
|
||||||
default:
|
default:
|
||||||
log.Println("Unknown SRT srtPacket type:", srtPacket.PacketType)
|
log.Println("Unknown SRT srtPacket type:", srtPacket.PacketType)
|
||||||
@ -156,3 +54,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{"-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",
|
||||||
|
"-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() {
|
||||||
|
inboundRTPPacket := make([]byte, 1500) // UDP MTU
|
||||||
|
for {
|
||||||
|
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() {
|
||||||
|
inboundRTPPacket := make([]byte, 1500) // UDP MTU
|
||||||
|
for {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/markbates/pkger"
|
"github.com/markbates/pkger"
|
||||||
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
|
||||||
"gitlab.crans.org/nounous/ghostream/stream/srt"
|
"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/stream/webrtc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,17 +24,18 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Get stream ID from URL, or from domain name
|
// Get stream ID from URL, or from domain name
|
||||||
path := r.URL.Path[1:]
|
path := r.URL.Path[1:]
|
||||||
if cfg.OneStreamPerDomain {
|
host := r.Host
|
||||||
host := r.Host
|
if strings.Contains(host, ":") {
|
||||||
if strings.Contains(host, ":") {
|
realHost, _, err := net.SplitHostPort(r.Host)
|
||||||
realHost, _, err := net.SplitHostPort(r.Host)
|
if err != nil {
|
||||||
if err != nil {
|
log.Printf("Failed to split host and port from %s", r.Host)
|
||||||
log.Printf("Failed to split host and port from %s", r.Host)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
host = realHost
|
|
||||||
}
|
}
|
||||||
path = host
|
host = realHost
|
||||||
|
}
|
||||||
|
host = strings.Replace(host, ".", "-", -1)
|
||||||
|
if streamID, ok := cfg.MapDomainToStream[host]; ok {
|
||||||
|
path = streamID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode client description
|
// Decode client description
|
||||||
@ -72,20 +74,21 @@ func viewerPostHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func viewerGetHandler(w http.ResponseWriter, r *http.Request) {
|
func viewerGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get stream ID from URL, or from domain name
|
// Get stream ID from URL, or from domain name
|
||||||
path := r.URL.Path[1:]
|
path := r.URL.Path[1:]
|
||||||
if cfg.OneStreamPerDomain {
|
host := r.Host
|
||||||
host := r.Host
|
if strings.Contains(host, ":") {
|
||||||
if strings.Contains(host, ":") {
|
realHost, _, err := net.SplitHostPort(r.Host)
|
||||||
realHost, _, err := net.SplitHostPort(r.Host)
|
if err != nil {
|
||||||
if err != nil {
|
log.Printf("Failed to split host and port from %s", r.Host)
|
||||||
log.Printf("Failed to split host and port from %s", r.Host)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
host = realHost
|
|
||||||
}
|
}
|
||||||
|
host = realHost
|
||||||
|
}
|
||||||
|
host = strings.Replace(host, ".", "-", -1)
|
||||||
|
if streamID, ok := cfg.MapDomainToStream[host]; ok {
|
||||||
if path == "about" {
|
if path == "about" {
|
||||||
path = ""
|
path = ""
|
||||||
} else {
|
} else {
|
||||||
path = host
|
path = streamID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +147,9 @@ func statisticsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
err := enc.Encode(struct {
|
err := enc.Encode(struct {
|
||||||
ConnectedViewers int
|
ConnectedViewers int
|
||||||
}{webrtc.GetNumberConnectedSessions(streamID) + srt.GetNumberConnectedSessions(streamID)})
|
}{webrtc.GetNumberConnectedSessions(streamID) +
|
||||||
|
srt.GetNumberConnectedSessions(streamID) +
|
||||||
|
telnet.GetNumberConnectedSessions(streamID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to generate JSON.", http.StatusInternalServerError)
|
http.Error(w, "Failed to generate JSON.", http.StatusInternalServerError)
|
||||||
log.Printf("Failed to generate JSON: %s", err)
|
log.Printf("Failed to generate JSON: %s", err)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
// Refresh viewer count by pulling metric from server
|
// Refresh viewer count by pulling metric from server
|
||||||
function refreshViewersCounter(period) {
|
function refreshViewersCounter(streamID, period) {
|
||||||
// Distinguish oneDomainPerStream mode
|
// Distinguish oneDomainPerStream mode
|
||||||
fetch("/_stats/" + (location.pathname === "/" ? location.host : location.pathname.substring(1)))
|
fetch("/_stats/" + streamID)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((data) => document.getElementById("connected-people").innerText = data.ConnectedViewers)
|
.then((data) => document.getElementById("connected-people").innerText = data.ConnectedViewers)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshViewersCounter(period)
|
refreshViewersCounter(streamID, period)
|
||||||
}, period)
|
}, period)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{{if .Path}}{{if not .Cfg.OneStreamPerDomain}}{{.Path}} - {{end}}{{end}}{{.Cfg.Name}}</title>
|
<title>{{if .Path}}{{.Path}} - {{end}}{{.Cfg.Name}}</title>
|
||||||
<link rel="stylesheet" href="static/css/style.css">
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
<link rel="stylesheet" href="static/css/player.css">
|
<link rel="stylesheet" href="static/css/player.css">
|
||||||
{{if .Cfg.CustomCSS}}<link rel="stylesheet" href="{{.Cfg.CustomCSS}}">{{end}}
|
{{if .Cfg.CustomCSS}}<link rel="stylesheet" href="{{.Cfg.CustomCSS}}">{{end}}
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
// Wait a bit before pulling viewers counter for the first time
|
// Wait a bit before pulling viewers counter for the first time
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshViewersCounter({{.Cfg.ViewersCounterRefreshPeriod}})
|
refreshViewersCounter("{{.Path}}", {{.Cfg.ViewersCounterRefreshPeriod}})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
@ -22,7 +22,7 @@ type Options struct {
|
|||||||
Hostname string
|
Hostname string
|
||||||
ListenAddress string
|
ListenAddress string
|
||||||
Name string
|
Name string
|
||||||
OneStreamPerDomain bool
|
MapDomainToStream map[string]string
|
||||||
PlayerPoster string
|
PlayerPoster string
|
||||||
SRTServerPort string
|
SRTServerPort string
|
||||||
STUNServers []string
|
STUNServers []string
|
||||||
|
Loading…
Reference in New Issue
Block a user