mirror of
				https://gitlab.crans.org/nounous/ghostream.git
				synced 2025-10-31 22:34:30 +01:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			h264-reade
			...
			1520e78bad
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 1520e78bad | ||
|  | e47aefd6df | ||
|  | 7e0ee7aba5 | ||
|  | 8d2adad509 | ||
|  | 0035c63c22 | ||
|  | 849196b4cb | ||
|  | 205c4b526c | ||
|  | 1d117ea480 | ||
|  | 45cb61e436 | ||
|  | 7e4adb475a | ||
|  | d1c4f81f4e | ||
|  | b2104a0cb7 | ||
|  | 6ca354f44f | ||
|  | a20c6202fd | ||
|  | b52f377b6b | ||
|  | 3d8ba0623d | ||
|  | cfcde6f530 | ||
|  | 28ef6a5526 | ||
|  | 5ad8a69c4c | ||
|  | d334556d2b | ||
|  | 9625cba5e1 | ||
|  | e74acf04f7 | ||
|  | 2085d13c0d | ||
|  | 85a5606291 | ||
|  | 33f86a0742 | ||
|  | 11d89c6950 | ||
|  | c9a2d5b359 | ||
|  | ee927c5b8f | ||
|  | 955364a5fc | 
| @@ -20,7 +20,7 @@ type Options struct { | ||||
|  | ||||
| // Backend to log user in | ||||
| type Backend interface { | ||||
| 	Login(string, string) (bool, error) | ||||
| 	Login(string, string) (bool, string, error) | ||||
| 	Close() | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,15 +23,15 @@ type Basic struct { | ||||
|  | ||||
| // Login hashs password and compare | ||||
| // Returns (true, nil) if success | ||||
| func (a Basic) Login(username string, password string) (bool, error) { | ||||
| func (a Basic) Login(username string, password string) (bool, string, error) { | ||||
| 	hash, ok := a.Cfg.Credentials[username] | ||||
| 	if !ok { | ||||
| 		return false, errors.New("user not found in credentials") | ||||
| 		return false, "", errors.New("user not found in credentials") | ||||
| 	} | ||||
| 	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) | ||||
|  | ||||
| 	// Login succeeded if no error | ||||
| 	return err == nil, err | ||||
| 	return err == nil, username, err | ||||
| } | ||||
|  | ||||
| // Close has no connection to close | ||||
|   | ||||
| @@ -3,12 +3,15 @@ package ldap | ||||
|  | ||||
| import ( | ||||
| 	"github.com/go-ldap/ldap/v3" | ||||
| 	"log" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // Options holds package configuration | ||||
| type Options struct { | ||||
| 	URI    string | ||||
| 	UserDn string | ||||
| 	Aliases map[string]map[string]string | ||||
| 	URI     string | ||||
| 	UserDn  string | ||||
| } | ||||
|  | ||||
| // LDAP authentification backend | ||||
| @@ -19,13 +22,35 @@ type LDAP struct { | ||||
|  | ||||
| // Login tries to bind to LDAP | ||||
| // Returns (true, nil) if success | ||||
| func (a LDAP) Login(username string, password string) (bool, error) { | ||||
| 	// Try to bind as user | ||||
| 	bindDn := "cn=" + username + "," + a.Cfg.UserDn | ||||
| 	err := a.Conn.Bind(bindDn, password) | ||||
| func (a LDAP) Login(username string, password string) (bool, string, error) { | ||||
| 	aliasSplit := strings.SplitN(username, "__", 2) | ||||
| 	potentialUsernames := []string{username} | ||||
|  | ||||
| 	// Login succeeded if no error | ||||
| 	return err == nil, err | ||||
| 	if len(aliasSplit) == 2 { | ||||
| 		alias := aliasSplit[0] | ||||
| 		trueUsername := aliasSplit[1] | ||||
| 		// Resolve stream alias if necessary | ||||
| 		if aliases, ok := a.Cfg.Aliases[alias]; ok { | ||||
| 			if _, ok := aliases[trueUsername]; ok { | ||||
| 				log.Printf("[LDAP] Use stream alias %s for username %s", alias, trueUsername) | ||||
| 				potentialUsernames = append(potentialUsernames, trueUsername) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var err error = nil | ||||
| 	for _, username := range potentialUsernames { | ||||
| 		// Try to bind as user | ||||
| 		bindDn := "cn=" + username + "," + a.Cfg.UserDn | ||||
| 		err = a.Conn.Bind(bindDn, password) | ||||
| 		if err == nil { | ||||
| 			// Login succeeded if no error | ||||
| 			return true, aliasSplit[0], nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Unable to log in | ||||
| 	return err == nil, "", err | ||||
| } | ||||
|  | ||||
| // Close LDAP connection | ||||
|   | ||||
							
								
								
									
										118
									
								
								docs/Server-docker.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								docs/Server-docker.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||
|  | ||||
| <Server version="7"> | ||||
|     <Name>OvenMediaEngine</Name> | ||||
|     <Type>origin</Type> | ||||
|     <IP>*</IP> | ||||
|  | ||||
|     <Bind> | ||||
|         <Providers> | ||||
|             <RTMP> | ||||
|                 <Port>1915</Port> | ||||
|             </RTMP> | ||||
|         </Providers> | ||||
|         <Publishers> | ||||
|             <WebRTC> | ||||
|                 <Signalling> | ||||
|                     <Port>3333</Port> | ||||
|                 </Signalling> | ||||
|                 <IceCandidates> | ||||
|                     <IceCandidate>*:10006-10010/udp</IceCandidate> | ||||
|                 </IceCandidates> | ||||
|             </WebRTC> | ||||
|             <HLS> | ||||
|               <Port>80</Port> | ||||
|             </HLS> | ||||
|             <DASH> | ||||
|                 <Port>80</Port> | ||||
|             </DASH> | ||||
|         </Publishers> | ||||
|     </Bind> | ||||
|  | ||||
|     <VirtualHosts> | ||||
|         <VirtualHost> | ||||
|             <Name>default</Name> | ||||
|             <Domain> | ||||
|                 <Names> | ||||
|                     <Name>*</Name> | ||||
|                 </Names> | ||||
|             </Domain> | ||||
|             <Applications> | ||||
|                 <Application> | ||||
|                     <Name>play</Name> | ||||
|                     <Type>live</Type> | ||||
|                     <Encodes> | ||||
|                         <Encode> | ||||
|                             <Name>opus_only</Name> | ||||
|                             <Audio> | ||||
|                                 <Codec>opus</Codec> | ||||
|                                 <Bitrate>128000</Bitrate> | ||||
|                                 <Samplerate>48000</Samplerate> | ||||
|                                 <Channel>2</Channel> | ||||
|                             </Audio> | ||||
|                             <Video> | ||||
|                                 <Bypass>true</Bypass> | ||||
|                             </Video> | ||||
|                         </Encode> | ||||
|                         <Encode> | ||||
|                             <Name>bypass</Name> | ||||
|                             <Audio> | ||||
|                                 <Bypass>true</Bypass> | ||||
|                             </Audio> | ||||
|                             <Video> | ||||
|                                 <Bypass>true</Bypass> | ||||
|                             </Video> | ||||
|                         </Encode> | ||||
|  | ||||
|                     </Encodes> | ||||
|                     <Streams> | ||||
|                         <Stream> | ||||
|                             <Name>${OriginStreamName}</Name> | ||||
|                             <Profiles> | ||||
|                                 <Profile>opus_only</Profile> | ||||
|                             </Profiles> | ||||
|                         </Stream> | ||||
|                         <Stream> | ||||
|                             <Name>${OriginStreamName}_bypass</Name> | ||||
|                             <Profiles> | ||||
|                                 <Profile>bypass</Profile> | ||||
|                             </Profiles> | ||||
|                         </Stream> | ||||
|  | ||||
|                     </Streams> | ||||
|                     <Providers> | ||||
|                         <RTMP> | ||||
|                             <BlockDuplicateStreamName>true</BlockDuplicateStreamName> | ||||
|                         </RTMP> | ||||
|                     </Providers> | ||||
|                     <Publishers> | ||||
|                         <ThreadCount>2</ThreadCount> | ||||
|                         <WebRTC> | ||||
|                             <Timeout>30000</Timeout> | ||||
|                         </WebRTC> | ||||
|                         <HLS> | ||||
|                             <SegmentDuration>2</SegmentDuration> | ||||
|                             <SegmentCount>2</SegmentCount> | ||||
|                             <CrossDomain> | ||||
|                                 <Url>*</Url> | ||||
|                             </CrossDomain> | ||||
|                         </HLS> | ||||
|                         <DASH> | ||||
|                             <SegmentDuration>2</SegmentDuration> | ||||
|                             <SegmentCount>2</SegmentCount> | ||||
|                             <CrossDomain> | ||||
|                                 <Url>*</Url> | ||||
|                             </CrossDomain> | ||||
|                         </DASH> | ||||
|                         <LLDASH> | ||||
|                             <SegmentDuration>2</SegmentDuration> | ||||
|                             <CrossDomain> | ||||
|                                 <Url>*</Url> | ||||
|                             </CrossDomain> | ||||
|                         </LLDASH> | ||||
|                     </Publishers> | ||||
|                 </Application> | ||||
|             </Applications> | ||||
|         </VirtualHost> | ||||
|     </VirtualHosts> | ||||
| </Server> | ||||
| @@ -26,11 +26,10 @@ services: | ||||
|       - "--certificatesResolvers.mytlschallenge.acme.httpChallenge.entryPoint=web" | ||||
|  | ||||
|   ghostream: | ||||
|     build: .. | ||||
|     build: https://gitlab.crans.org/nounous/ghostream.git | ||||
|     restart: always | ||||
|     ports: | ||||
|       - 9710:9710/udp | ||||
|       - 10000-11000:10000-11000/udp | ||||
|     volumes: | ||||
|       - ./ghostream_data:/etc/ghostream:ro | ||||
|     labels: | ||||
| @@ -40,3 +39,30 @@ services: | ||||
|       - "traefik.http.routers.ghostream.tls.certresolver=mytlschallenge" | ||||
|       - "traefik.http.routers.ghostream.service=ghostream" | ||||
|       - "traefik.http.services.ghostream.loadbalancer.server.port=8080" | ||||
|  | ||||
|   ovenmediaengine: | ||||
|     image: airensoft/ovenmediaengine:0.10.8 | ||||
|     restart: always | ||||
|     ports: | ||||
|       # WebRTC ICE | ||||
|       - 10006-10010:10006-10010/udp | ||||
|     volumes: | ||||
|       - ./ovenmediaengine_data/conf/Server-docker.xml:/opt/ovenmediaengine/bin/origin_conf/Server.xml:ro | ||||
|     labels: | ||||
|       - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https" | ||||
|  | ||||
|       - "traefik.http.routers.ovenmediaengine.rule=Host(`stream.example.com`) && PathPrefix(`/play/`)" | ||||
|       - "traefik.http.routers.ovenmediaengine.priority=101" | ||||
|       - "traefik.http.routers.ovenmediaengine.entrypoints=websecure" | ||||
|       - "traefik.http.routers.ovenmediaengine.tls.certresolver=mytlschallenge" | ||||
|       - "traefik.http.services.ovenmediaengine.loadbalancer.server.port=3333" | ||||
|       - "traefik.http.routers.ovenmediaengine.service=ovenmediaengine" | ||||
|       - "traefik.http.routers.ovenmediaengine.middlewares=sslheader" | ||||
|  | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.rule=Host(`stream.example.com`) && Path(`/play/{app_name:.*}/{filename:.*}.{ext:(m3u8|mpd|ts)}`)" | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.priority=102" | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.entrypoints=websecure" | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.tls.certresolver=mytlschallenge" | ||||
|       - "traefik.http.services.ovenmediaengine-hls.loadbalancer.server.port=80" | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.service=ovenmediaengine-hls" | ||||
|       - "traefik.http.routers.ovenmediaengine-hls.middlewares=sslheader" | ||||
|   | ||||
| @@ -34,6 +34,13 @@ auth: | ||||
|   #ldap: | ||||
|   #  uri: ldap://127.0.0.1:389 | ||||
|   #  userdn: cn=users,dc=example,dc=com | ||||
|   # | ||||
|   #  # You can define aliases, to stream on stream.example.com/example with the credentials of the demo account. | ||||
|   #  # You will have to use the streamid example__demo:password | ||||
|   #  aliases: | ||||
|   #    example: | ||||
|   #      demo: ignored | ||||
|   # | ||||
|  | ||||
| ## Stream forwarding ## | ||||
| # Forward an incoming stream to other servers | ||||
| @@ -61,6 +68,19 @@ monitoring: | ||||
|   # To limit access to only localhost, use 127.0.0.1:2112 | ||||
|   #listenAddress: :2112 | ||||
|  | ||||
| ## OvenMediaEngine ## | ||||
| # Send the stream data to OvenMediaEngine to handle properly the web client | ||||
| ome: | ||||
|   # If you disable OME module, the laggy webrtc client will be used. | ||||
|   # | ||||
|   #enabled: true | ||||
|   # | ||||
|   # The URL where OME listens RTMP, without the prefix. | ||||
|   #url: ovenmediaengine:1915 | ||||
|   # | ||||
|   # The OME app where OME is waiting for the data of Ghostream. | ||||
|   #app: play | ||||
|  | ||||
| ## SRT server ## | ||||
| # The SRT server receive incoming stream and can also serve video to clients. | ||||
| srt: | ||||
| @@ -160,11 +180,23 @@ web: | ||||
|   # | ||||
|   #widgetURL: "" | ||||
|  | ||||
|   # IMPORTANT, CHANGE THIS | ||||
|   # You need to declare which entity you are and to specify an address to claim some content. | ||||
|   legalMentionsEntity: "l'association Crans" | ||||
|   legalMentionsAddress: "61 Avenue du Président Wilson, 94235 Cachan Cedex, France" | ||||
|   legalMentionsFullAddress: | ||||
|     - Association Cr@ns - ENS Paris-Saclay | ||||
|     - Notification de Contenus Illicites | ||||
|     - 4, avenue des Sciences | ||||
|     - 91190 Gif-sur-Yvette | ||||
|     - France | ||||
|   legalMentionsEmail: "bureau[at]crans.org" | ||||
|  | ||||
| ## WebRTC server ## | ||||
| webrtc: | ||||
|   # If you disable webrtc module, the web client won't be able to play streams. | ||||
|   # | ||||
|   #enabled: true | ||||
|   #enabled: false | ||||
|  | ||||
|   # UDP port range used to stream | ||||
|   # This range must be opened in your firewall. | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||
| 	"net" | ||||
|  | ||||
| 	"github.com/sherifabdlnaby/configuro" | ||||
| @@ -23,6 +24,7 @@ type Config struct { | ||||
| 	Auth       auth.Options | ||||
| 	Forwarding forwarding.Options | ||||
| 	Monitoring monitoring.Options | ||||
| 	OME        ovenmediaengine.Options | ||||
| 	Srt        srt.Options | ||||
| 	Telnet     telnet.Options | ||||
| 	Transcoder transcoder.Options | ||||
| @@ -40,8 +42,9 @@ func New() *Config { | ||||
| 				Credentials: make(map[string]string), | ||||
| 			}, | ||||
| 			LDAP: ldap.Options{ | ||||
| 				URI:    "ldap://127.0.0.1:389", | ||||
| 				UserDn: "cn=users,dc=example,dc=com", | ||||
| 				Aliases: make(map[string]map[string]string), | ||||
| 				URI:     "ldap://127.0.0.1:389", | ||||
| 				UserDn:  "cn=users,dc=example,dc=com", | ||||
| 			}, | ||||
| 		}, | ||||
| 		Forwarding: make(map[string][]string), | ||||
| @@ -49,6 +52,11 @@ func New() *Config { | ||||
| 			Enabled:       true, | ||||
| 			ListenAddress: ":2112", | ||||
| 		}, | ||||
| 		OME: ovenmediaengine.Options{ | ||||
| 			Enabled: true, | ||||
| 			URL:     "ovenmediaengine:1915", | ||||
| 			App:     "play", | ||||
| 		}, | ||||
| 		Srt: srt.Options{ | ||||
| 			Enabled:       true, | ||||
| 			ListenAddress: ":9710", | ||||
| @@ -75,9 +83,14 @@ func New() *Config { | ||||
| 			MapDomainToStream:           make(map[string]string), | ||||
| 			PlayerPoster:                "/static/img/no_stream.svg", | ||||
| 			ViewersCounterRefreshPeriod: 20000, | ||||
| 			LegalMentionsEntity:         "l'association Crans", | ||||
| 			LegalMentionsAddress:        "61 Avenue du Président Wilson, 94235 Cachan Cedex, France", | ||||
| 			LegalMentionsFullAddress: []string{"Association Cr@ns - ENS Paris-Saclay", | ||||
| 				"Notification de Contenus Illicites", "4, avenue des Sciences", "91190 Gif-sur-Yvette", "France"}, | ||||
| 			LegalMentionsEmail: "bureau[at]crans.org", | ||||
| 		}, | ||||
| 		WebRTC: webrtc.Options{ | ||||
| 			Enabled:     true, | ||||
| 			Enabled:     false, | ||||
| 			MaxPortUDP:  11000, | ||||
| 			MinPortUDP:  10000, | ||||
| 			STUNServers: []string{"stun:stun.l.google.com:19302"}, | ||||
|   | ||||
							
								
								
									
										4
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								main.go
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||
| 	"log" | ||||
|  | ||||
| 	"github.com/pkg/profile" | ||||
| @@ -49,9 +50,10 @@ func main() { | ||||
| 	go transcoder.Init(streams, &cfg.Transcoder) | ||||
| 	go forwarding.Serve(streams, cfg.Forwarding) | ||||
| 	go monitoring.Serve(&cfg.Monitoring) | ||||
| 	go ovenmediaengine.Serve(streams, &cfg.OME) | ||||
| 	go srt.Serve(streams, authBackend, &cfg.Srt) | ||||
| 	go telnet.Serve(streams, &cfg.Telnet) | ||||
| 	go web.Serve(streams, &cfg.Web) | ||||
| 	go web.Serve(streams, &cfg.Web, &cfg.OME) | ||||
| 	go webrtc.Serve(streams, &cfg.WebRTC) | ||||
|  | ||||
| 	// Wait for routines | ||||
|   | ||||
| @@ -40,6 +40,7 @@ func Serve(streams *messaging.Streams, cfg Options) { | ||||
| 		stream, err := streams.Get(name) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Failed to get stream '%s'", name) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Get specific quality | ||||
| @@ -74,8 +75,8 @@ func forward(streamName string, q *messaging.Quality, fwdCfg []string) { | ||||
| 		formattedURL = strings.ReplaceAll(formattedURL, "%S", fmt.Sprintf("%02d", now.Second())) | ||||
| 		formattedURL = strings.ReplaceAll(formattedURL, "%name", streamName) | ||||
|  | ||||
| 		params = append(params, "-f", "flv", "-preset", "ultrafast", "-tune", "zerolatency", | ||||
| 			"-c", "copy", formattedURL) | ||||
| 		params = append(params, "-f", "flv", | ||||
| 			"-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-ar", "44100", formattedURL) | ||||
| 	} | ||||
| 	ffmpeg := exec.Command("ffmpeg", params...) | ||||
|  | ||||
|   | ||||
							
								
								
									
										112
									
								
								stream/ovenmediaengine/ovenmediaengine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								stream/ovenmediaengine/ovenmediaengine.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| // Package ovenmediaengine provides the forwarding to an ovenmediaengine server to handle the web client | ||||
| package ovenmediaengine | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os/exec" | ||||
|  | ||||
| 	"gitlab.crans.org/nounous/ghostream/messaging" | ||||
| ) | ||||
|  | ||||
| // Options holds ovenmediaengine package configuration | ||||
| type Options struct { | ||||
| 	Enabled bool | ||||
| 	URL     string | ||||
| 	App     string | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	cfg *Options | ||||
| ) | ||||
|  | ||||
| // Serve handles incoming packets from SRT and forward them to OME | ||||
| func Serve(streams *messaging.Streams, c *Options) { | ||||
| 	cfg = c | ||||
|  | ||||
| 	if !c.Enabled { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Subscribe to new stream event | ||||
| 	event := make(chan string, 8) | ||||
| 	streams.Subscribe(event) | ||||
| 	log.Printf("Stream forwarding to OME initialized") | ||||
|  | ||||
| 	// For each new stream | ||||
| 	for name := range event { | ||||
| 		// Get stream | ||||
| 		stream, err := streams.Get(name) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Failed to get stream '%s'", name) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		qualityName := "source" | ||||
| 		quality, err := stream.GetQuality(qualityName) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Failed to get quality '%s'", qualityName) | ||||
| 		} | ||||
|  | ||||
| 		// Start forwarding | ||||
| 		log.Printf("Starting forwarding to OME for '%s'", name) | ||||
| 		go forward(name, quality) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Start a FFMPEG instance and redirect stream output to OME | ||||
| func forward(name string, q *messaging.Quality) { | ||||
| 	output := make(chan []byte, 1024) | ||||
| 	q.Register(output) | ||||
|  | ||||
| 	// TODO When a new OME version got released with SRT support, directly forward SRT packets, without using unwanted RTMP transport | ||||
| 	// Launch FFMPEG instance | ||||
| 	params := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0", "-f", "flv", "-c:v", "copy", | ||||
| 		"-c:a", "aac", "-b:a", "160k", "-ar", "44100", | ||||
| 		fmt.Sprintf("rtmp://%s/%s/%s", cfg.URL, cfg.App, name)} | ||||
| 	ffmpeg := exec.Command("ffmpeg", params...) | ||||
|  | ||||
| 	// Open pipes | ||||
| 	input, err := ffmpeg.StdinPipe() | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error while opening forwarding ffmpeg input pipe: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	errOutput, err := ffmpeg.StderrPipe() | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error while opening forwarding ffmpeg output pipe: %s", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Start FFMpeg | ||||
| 	if err := ffmpeg.Start(); err != nil { | ||||
| 		log.Printf("Error while starting forwarding ffmpeg instance: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	// Kill FFMPEG when stream is ended | ||||
| 	defer func() { | ||||
| 		_ = input.Close() | ||||
| 		_ = errOutput.Close() | ||||
| 		_ = ffmpeg.Process.Kill() | ||||
| 		q.Unregister(output) | ||||
| 	}() | ||||
|  | ||||
| 	// Log standard error output | ||||
| 	go func() { | ||||
| 		scanner := bufio.NewScanner(errOutput) | ||||
| 		for scanner.Scan() { | ||||
| 			log.Printf("[FORWARDING OME FFMPEG %s] %s", name, scanner.Text()) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	// Read stream output and redirect immediately to ffmpeg | ||||
| 	for data := range output { | ||||
| 		_, err := input.Write(data) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			log.Printf("Error while writing to forwarded stream: %s", err) | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -82,7 +82,9 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) { | ||||
| 			name, password := split[0], split[1] | ||||
| 			if authBackend != nil { | ||||
| 				// check password | ||||
| 				if ok, err := authBackend.Login(name, password); !ok || err != nil { | ||||
| 				ok, username, err := authBackend.Login(name, password) | ||||
| 				name = username | ||||
| 				if ok || err != nil { | ||||
| 					log.Printf("Failed to authenticate for stream %s", name) | ||||
| 					s.Close() | ||||
| 					continue | ||||
|   | ||||
| @@ -10,15 +10,21 @@ import ( | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/markbates/pkger" | ||||
| 	"gitlab.crans.org/nounous/ghostream/internal/monitoring" | ||||
| 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||
| 	"gitlab.crans.org/nounous/ghostream/stream/webrtc" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// Precompile regex | ||||
| 	validPath = regexp.MustCompile("^/[a-z0-9@_-]*$") | ||||
|  | ||||
| 	counterMutex     = new(sync.Mutex) | ||||
| 	connectedClients = make(map[string]map[string]int64) | ||||
| ) | ||||
|  | ||||
| // Handle site index and viewer pages | ||||
| @@ -61,7 +67,8 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 		Cfg       *Options | ||||
| 		Path      string | ||||
| 		WidgetURL string | ||||
| 	}{Path: path, Cfg: cfg, WidgetURL: ""} | ||||
| 		OMECfg    *ovenmediaengine.Options | ||||
| 	}{Path: path, Cfg: cfg, WidgetURL: "", OMECfg: omeCfg} | ||||
|  | ||||
| 	// Load widget is user does not disable it with ?nowidget | ||||
| 	if _, ok := r.URL.Query()["nowidget"]; !ok { | ||||
| @@ -88,14 +95,43 @@ func staticHandler() http.Handler { | ||||
| } | ||||
|  | ||||
| func statisticsHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	// Retrieve stream name from URL | ||||
| 	name := strings.SplitN(strings.Replace(r.URL.Path[7:], "/", "", -1), "@", 2)[0] | ||||
| 	userCount := 0 | ||||
|  | ||||
| 	// Clients have a unique generated identifier per session, that expires in 40 seconds. | ||||
| 	// Each time the client connects to this page, the identifier is renewed. | ||||
| 	// Yeah, that's not a good way to have stats, but it works... | ||||
| 	if connectedClients[name] == nil { | ||||
| 		counterMutex.Lock() | ||||
| 		connectedClients[name] = make(map[string]int64) | ||||
| 		counterMutex.Unlock() | ||||
| 	} | ||||
| 	currentTime := time.Now().Unix() | ||||
| 	if _, ok := r.URL.Query()["uid"]; ok { | ||||
| 		uid := r.URL.Query()["uid"][0] | ||||
| 		counterMutex.Lock() | ||||
| 		connectedClients[name][uid] = currentTime | ||||
| 		counterMutex.Unlock() | ||||
| 	} | ||||
| 	toDelete := make([]string, 0) | ||||
| 	counterMutex.Lock() | ||||
| 	for uid, oldTime := range connectedClients[name] { | ||||
| 		if currentTime-oldTime > 40 { | ||||
| 			toDelete = append(toDelete, uid) | ||||
| 		} | ||||
| 	} | ||||
| 	for _, uid := range toDelete { | ||||
| 		delete(connectedClients[name], uid) | ||||
| 	} | ||||
| 	counterMutex.Unlock() | ||||
|  | ||||
| 	// Get requested stream | ||||
| 	stream, err := streams.Get(name) | ||||
| 	if err == nil { | ||||
| 		userCount = stream.ClientCount() | ||||
| 		userCount += webrtc.GetNumberConnectedSessions(name) | ||||
| 		userCount += len(connectedClients[name]) | ||||
| 	} | ||||
|  | ||||
| 	// Display connected users statistics | ||||
|   | ||||
| @@ -9,6 +9,7 @@ export class ViewerCounter { | ||||
|     constructor(element, streamName) { | ||||
|         this.element = element; | ||||
|         this.url = "/_stats/" + streamName; | ||||
|         this.uid = Math.floor(1e19 * Math.random()).toString(16); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -21,7 +22,7 @@ export class ViewerCounter { | ||||
|     } | ||||
|  | ||||
|     refreshViewersCounter() { | ||||
|         fetch(this.url) | ||||
|         fetch(this.url + "?uid=" + this.uid) | ||||
|             .then(response => response.json()) | ||||
|             .then((data) => this.element.innerText = data.ConnectedViewers) | ||||
|             .catch(console.log); | ||||
|   | ||||
							
								
								
									
										116
									
								
								web/static/js/ovenplayer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								web/static/js/ovenplayer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import { ViewerCounter } from "./modules/viewerCounter.js"; | ||||
|  | ||||
| /** | ||||
|  * Initialize viewer page | ||||
|  * | ||||
|  * @param {String} stream | ||||
|  * @param {String} omeApp | ||||
|  * @param {Number} viewersCounterRefreshPeriod | ||||
|  * @param {String} posterUrl | ||||
|  */ | ||||
| export function initViewerPage(stream, omeApp, viewersCounterRefreshPeriod, posterUrl) { | ||||
|     // Create viewer counter | ||||
|     const viewerCounter = new ViewerCounter( | ||||
|         document.getElementById("connected-people"), | ||||
|         stream, | ||||
|     ); | ||||
|     viewerCounter.regularUpdate(viewersCounterRefreshPeriod); | ||||
|     viewerCounter.refreshViewersCounter(); | ||||
|  | ||||
|     // Side widget toggler | ||||
|     const sideWidgetToggle = document.getElementById("sideWidgetToggle"); | ||||
|     const sideWidget = document.getElementById("sideWidget"); | ||||
|     if (sideWidgetToggle !== null && sideWidget !== null) { | ||||
|         // On click, toggle side widget visibility | ||||
|         sideWidgetToggle.addEventListener("click", function () { | ||||
|             if (sideWidget.style.display === "none") { | ||||
|                 sideWidget.style.display = "block"; | ||||
|                 sideWidgetToggle.textContent = "»"; | ||||
|             } else { | ||||
|                 sideWidget.style.display = "none"; | ||||
|                 sideWidgetToggle.textContent = "«"; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Create player | ||||
|     let player = OvenPlayer.create("viewer", { | ||||
|         title: stream, | ||||
|         image: posterUrl, | ||||
|         autoStart: true, | ||||
|         mute: true, | ||||
|         expandFullScreenUI: true, | ||||
|         sources: [ | ||||
|             { | ||||
|                 "file": "wss://" + window.location.host + "/" + omeApp + "/" + stream, | ||||
|                 "type": "webrtc", | ||||
|                 "label": " WebRTC - Source" | ||||
|             }, | ||||
|             { | ||||
|                 "file": "https://" + window.location.host + "/" + omeApp + "/" + stream + "_bypass/playlist.m3u8", | ||||
|                 "type": "hls", | ||||
|                 "label": " HLS" | ||||
|             }, | ||||
|             { | ||||
|                 "file": "https://" + window.location.host + "/" + omeApp + "/" + stream + "_bypass/manifest.mpd", | ||||
|                 "type": "dash", | ||||
|                 "label": "DASH" | ||||
|             }, | ||||
|             { | ||||
|                 "file": "https://" + window.location.host + "/" + omeApp + "/" + stream + "_bypass/manifest_ll.mpd", | ||||
|                 "type": "dash", | ||||
|                 "label": "LL-DASH" | ||||
|             }, | ||||
|         ] | ||||
|     }); | ||||
|     player.on("stateChanged", function (data) { | ||||
|         if (data.newstate === "loading") { | ||||
|             document.getElementById("connectionIndicator").style.fill = '#ffc107' | ||||
|         } | ||||
|         if (data.newstate === "playing") { | ||||
|             document.getElementById("connectionIndicator").style.fill = '#28a745' | ||||
|         } | ||||
|         if (data.newstate === "idle") { | ||||
|             document.getElementById("connectionIndicator").style.fill = '#dc3545' | ||||
|         } | ||||
|     }) | ||||
|     player.on("error", function (error) { | ||||
|         document.getElementById("connectionIndicator").style.fill = '#dc3545' | ||||
|         if (error.code === 501 || error.code === 406) { | ||||
|             // Clear messages | ||||
|             const errorMsg = document.getElementsByClassName("op-message-text")[0] | ||||
|             errorMsg.textContent = "" | ||||
|  | ||||
|             const warningIcon = document.getElementsByClassName("op-message-icon")[0] | ||||
|             warningIcon.textContent = "" | ||||
|  | ||||
|             // Reload in 30s | ||||
|             setTimeout(function () { | ||||
|                 player.load() | ||||
|             }, 30000) | ||||
|         } else { | ||||
|             console.log(error); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Register keyboard events | ||||
|     window.addEventListener("keydown", (event) => { | ||||
|         switch (event.key) { | ||||
|             case "f": | ||||
|                 // F key put player in fullscreen | ||||
|                 if (document.fullscreenElement !== null) { | ||||
|                     document.exitFullscreen() | ||||
|                 } else { | ||||
|                     document.getElementsByTagName("video")[0].requestFullscreen() | ||||
|                 } | ||||
|                 break; | ||||
|             case "m": | ||||
|             case " ": | ||||
|                 // M and space key mute player | ||||
|                 player.setMute(!player.getMute()) | ||||
|                 event.preventDefault() | ||||
|                 player.play() | ||||
|                 break; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										2
									
								
								web/static/ovenplayer/ovenplayer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/static/ovenplayer/ovenplayer.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								web/static/ovenplayer/ovenplayer.js.LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/static/ovenplayer/ovenplayer.js.LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
							
								
								
									
										2
									
								
								web/static/ovenplayer/ovenplayer.provider.Html5-0.9.0.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/static/ovenplayer/ovenplayer.provider.Html5-0.9.0.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
							
								
								
									
										2
									
								
								web/static/ovenplayer/ovenplayer.sdk.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/static/ovenplayer/ovenplayer.sdk.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								web/static/ovenplayer/ovenplayer.sdk.js.LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/static/ovenplayer/ovenplayer.sdk.js.LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /*! OvenPlayerv0.9.0 | (c)2020 AirenSoft Co., Ltd. | MIT license (https://github.com/AirenSoft/OvenPlayerPrivate/blob/master/LICENSE) | Github : https://github.com/AirenSoft/OvenPlayer */ | ||||
| @@ -9,7 +9,11 @@ | ||||
|   </p> | ||||
|  | ||||
|   <h2>Comment je diffuse ?</h2> | ||||
|   <p>Pour diffuser un contenu vous devez être adhérent Crans.</p> | ||||
|   <p> | ||||
|     Pour diffuser un contenu vous devez avoir des identifiants valides. | ||||
|     Si le service est hébergé par une association, il est probable que | ||||
|     vous deviez être membre de cette association. | ||||
|   </p> | ||||
|  | ||||
|   <h3>Avec Open Broadcaster Software</h3> | ||||
|   <p> | ||||
| @@ -21,7 +25,7 @@ | ||||
|   <ul> | ||||
|     <li> | ||||
|       <b>Serveur :</b> | ||||
|       <code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?IDENTIFIANT:MOT_DE_PASS</code>, | ||||
|       <code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?IDENTIFIANT:MOT_DE_PASSE</code>, | ||||
|       avec <code>IDENTIFIANT</code> et <code>MOT_DE_PASSE</code> | ||||
|       vos identifiants. | ||||
|     </li> | ||||
| @@ -42,7 +46,8 @@ | ||||
|   <p> | ||||
|     <code> | ||||
|       {{/* FIXME replace with good SRT params */}} | ||||
|       ffmpeg -re -i mavideo.webm -vcodec libx264 -vprofile baseline | ||||
|       ffmpeg -re -i mavideo.webm -vcodec libx264 | ||||
|       -preset:v veryfast -vprofile baseline -tune zerolatency | ||||
|       -acodec aac -strict -2 -f flv | ||||
|       srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT:MOT_DE_PASSE | ||||
|     </code> | ||||
| @@ -95,10 +100,9 @@ | ||||
|     Bien que VLC supporte officiellement le protocole SRT, | ||||
|     toutes les options ne sont pas encore implémentées, | ||||
|     notamment l'option pour choisir son stream. | ||||
|     <a href="https://patches.videolan.org/patch/30299/">Un patch</a> | ||||
|     a été soumis et est en attente d'acceptation. | ||||
|     Une fois le patch accepté, il sera appliqué dans les versions | ||||
|     de développement de VLC. Sous Arch Linux, il suffit de récupérer | ||||
|     Cette option n'est supportée que dans la version de développement | ||||
|     depuis très récemment, grâce à un patch de l'un des développeurs | ||||
|     de Ghostream.  Sous Arch Linux, il suffit de récupérer | ||||
|     le paquet <code>vlc-git</code> de l'AUR. Avec un VLC à jour, | ||||
|     il suffit d'exécuter : | ||||
|   </p> | ||||
| @@ -128,18 +132,18 @@ | ||||
|     Le service de diffusion vidéo du Crans est un service d'hébergement | ||||
|     au sens de l'article 6, I, 2e de la loi 2004-575 du 21 juin 2004. | ||||
|     Conformément aux dispositions de l'article 6, II du même, | ||||
|     l'association Crans conserve les données de nature à permettre | ||||
|      conserve les données de nature à permettre | ||||
|     l'identification des auteurs du contenu diffusé. | ||||
|     Ce service est hébergé par l'association Crans, au | ||||
|     61 Avenue du Président Wilson, 94235 Cachan Cedex, France. | ||||
|     Ce service est hébergé par {{.Cfg.LegalMentionsEntity}}, au | ||||
|     {{.Cfg.LegalMentionsAddress}}. | ||||
|   </p> | ||||
|   <p> | ||||
|     <b>En cas de réclamation sur le contenu diffusé</b>, | ||||
|     la loi vous autorise à contacter directement l'hébergeur à | ||||
|     l'adresse suivante : | ||||
|     <pre>Association Cr@ns - ENS Paris-Saclay<br/>Notification de Contenus Illicites<br/>4, avenue des Sciences<br/>91190 Gif-sur-Yvette<br/>France</pre> | ||||
|     <pre>{{range $i, $element := .Cfg.LegalMentionsFullAddress}}{{$element}}<br/>{{end}}</pre> | ||||
|     Vous pouvez également envoyer directement vos réclamations par | ||||
|     courrier électronique à l'adresse <code>bureau[at]crans.org</code>. | ||||
|     courrier électronique à l'adresse <code>{{.Cfg.LegalMentionsEmail}}</code>. | ||||
|   </p> | ||||
| </div> | ||||
| {{end}} | ||||
|   | ||||
| @@ -6,14 +6,14 @@ | ||||
|  | ||||
|     <!-- Links and settings under video --> | ||||
|     <div class="controls"> | ||||
|       <span class="control-quality"> | ||||
|       <!-- <span class="control-quality"> | ||||
|         <select id="quality"> | ||||
|           <option value="source">Source</option> | ||||
|           <option value="720p">720p</option> | ||||
|           <option value="480p">480p</option> | ||||
|           <option value="240p">240p</option> | ||||
|         </select>   | ||||
|       </span> | ||||
|       </span> --> | ||||
|       <code class="control-srt-link">srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid={{.Path}}</code> | ||||
|       <span class="control-viewers" id="connected-people">0</span> | ||||
|       <svg class="control-indicator" id="connectionIndicator" fill="#dc3545" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> | ||||
| @@ -34,8 +34,18 @@ | ||||
|   {{end}} | ||||
| </div> | ||||
|  | ||||
| {{if .OMECfg.Enabled}} | ||||
|   <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> | ||||
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/dashjs/2.9.3/dash.all.min.js"></script> | ||||
|   <script src="/static/ovenplayer/ovenplayer.js"></script> | ||||
|   <script src="/static/js/ovenplayer.js"></script> | ||||
| {{end}} | ||||
| <script type="module"> | ||||
|   import { initViewerPage } from "/static/js/viewer.js"; | ||||
|   {{if .OMECfg.Enabled}} | ||||
|     import { initViewerPage } from "/static/js/ovenplayer.js"; | ||||
|   {{else}} | ||||
|     import { initViewerPage } from "/static/js/viewer.js"; | ||||
|   {{end}} | ||||
|  | ||||
|   // Some variables that need to be fixed by web page | ||||
|   const viewersCounterRefreshPeriod = Number("{{.Cfg.ViewersCounterRefreshPeriod}}"); | ||||
| @@ -45,6 +55,10 @@ | ||||
|     "{{$value}}", | ||||
|     {{end}} | ||||
|   ] | ||||
|   initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) | ||||
|   {{if .OMECfg.Enabled}} | ||||
|     initViewerPage(stream, {{.OMECfg.App}}, viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}}) | ||||
|   {{else}} | ||||
|     initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) | ||||
|   {{end}} | ||||
| </script> | ||||
| {{end}} | ||||
|   | ||||
							
								
								
									
										10
									
								
								web/web.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								web/web.go
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ | ||||
| package web | ||||
|  | ||||
| import ( | ||||
| 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||
| 	"html/template" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| @@ -27,11 +28,17 @@ type Options struct { | ||||
| 	STUNServers                 []string | ||||
| 	ViewersCounterRefreshPeriod int | ||||
| 	WidgetURL                   string | ||||
| 	LegalMentionsEntity         string | ||||
| 	LegalMentionsAddress        string | ||||
| 	LegalMentionsFullAddress    []string | ||||
| 	LegalMentionsEmail          string | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	cfg *Options | ||||
|  | ||||
| 	omeCfg *ovenmediaengine.Options | ||||
|  | ||||
| 	// Preload templates | ||||
| 	templates *template.Template | ||||
|  | ||||
| @@ -70,9 +77,10 @@ func loadTemplates() error { | ||||
| } | ||||
|  | ||||
| // Serve HTTP server | ||||
| func Serve(s *messaging.Streams, c *Options) { | ||||
| func Serve(s *messaging.Streams, c *Options, ome *ovenmediaengine.Options) { | ||||
| 	streams = s | ||||
| 	cfg = c | ||||
| 	omeCfg = ome | ||||
|  | ||||
| 	if !cfg.Enabled { | ||||
| 		// Web server is not enabled, ignore | ||||
|   | ||||
		Reference in New Issue
	
	Block a user