mirror of
				https://gitlab.crans.org/nounous/ghostream.git
				synced 2025-10-31 10:39:51 +01:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			multi-qual
			...
			8d2adad509
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | ||
|  | fc3c0f606c | ||
|  | e1bdae5380 | ||
|  | 26e7182085 | ||
|  | 59c47ca3e0 | ||
|  | 8eea7d6e4f | ||
|  | f394af9257 | ||
|  | 5b40aa886f | ||
|  | 9fc3d37e72 | 
| @@ -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 | ||||||
| # 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-10005 (UDP) for WebRTC | # 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-11000 (UDP) for WebRTC | ||||||
| EXPOSE 2112 8023 8080 9710/udp 10000-10005/udp | EXPOSE 2112 8023 8080 9710/udp 10000-11000/udp | ||||||
| CMD ["/app/ghostream"] | CMD ["/app/ghostream"] | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import ( | |||||||
|  |  | ||||||
| func TestBasicLogin(t *testing.T) { | func TestBasicLogin(t *testing.T) { | ||||||
| 	basicCredentials := make(map[string]string) | 	basicCredentials := make(map[string]string) | ||||||
| 	basicCredentials["demo"] = "$2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6" | 	basicCredentials["demo"] = "$2b$10$xuU7XFwmRX2CMgdSaA8rM.4Y8.BtRNzhUedwN0G8tCegDRNUERTCS" | ||||||
|  |  | ||||||
| 	// Test good credentials | 	// Test good credentials | ||||||
| 	backend, _ := New(&Options{Credentials: basicCredentials}) | 	backend, _ := New(&Options{Credentials: basicCredentials}) | ||||||
|   | |||||||
| @@ -3,10 +3,12 @@ package ldap | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/go-ldap/ldap/v3" | 	"github.com/go-ldap/ldap/v3" | ||||||
|  | 	"log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Options holds package configuration | // Options holds package configuration | ||||||
| type Options struct { | type Options struct { | ||||||
|  | 	Aliases map[string]string | ||||||
| 	URI     string | 	URI     string | ||||||
| 	UserDn  string | 	UserDn  string | ||||||
| } | } | ||||||
| @@ -20,6 +22,12 @@ type LDAP struct { | |||||||
| // Login tries to bind to LDAP | // Login tries to bind to LDAP | ||||||
| // Returns (true, nil) if success | // Returns (true, nil) if success | ||||||
| func (a LDAP) Login(username string, password string) (bool, error) { | func (a LDAP) Login(username string, password string) (bool, error) { | ||||||
|  | 	// Resolve stream alias if necessary | ||||||
|  | 	for aliasFor, ok := a.Cfg.Aliases[username]; ok; aliasFor, ok = a.Cfg.Aliases[username] { | ||||||
|  | 		log.Printf("[LDAP] Use stream alias %s for username %s", username, aliasFor) | ||||||
|  | 		username = aliasFor | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Try to bind as user | 	// Try to bind as user | ||||||
| 	bindDn := "cn=" + username + "," + a.Cfg.UserDn | 	bindDn := "cn=" + username + "," + a.Cfg.UserDn | ||||||
| 	err := a.Conn.Bind(bindDn, password) | 	err := a.Conn.Bind(bindDn, password) | ||||||
|   | |||||||
							
								
								
									
										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" |       - "--certificatesResolvers.mytlschallenge.acme.httpChallenge.entryPoint=web" | ||||||
|  |  | ||||||
|   ghostream: |   ghostream: | ||||||
|     build: .. |     build: https://gitlab.crans.org/nounous/ghostream.git | ||||||
|     restart: always |     restart: always | ||||||
|     ports: |     ports: | ||||||
|       - 9710:9710/udp |       - 9710:9710/udp | ||||||
|       - 10000-10005:10000-10005/udp |  | ||||||
|     volumes: |     volumes: | ||||||
|       - ./ghostream_data:/etc/ghostream:ro |       - ./ghostream_data:/etc/ghostream:ro | ||||||
|     labels: |     labels: | ||||||
| @@ -40,3 +39,30 @@ services: | |||||||
|       - "traefik.http.routers.ghostream.tls.certresolver=mytlschallenge" |       - "traefik.http.routers.ghostream.tls.certresolver=mytlschallenge" | ||||||
|       - "traefik.http.routers.ghostream.service=ghostream" |       - "traefik.http.routers.ghostream.service=ghostream" | ||||||
|       - "traefik.http.services.ghostream.loadbalancer.server.port=8080" |       - "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" | ||||||
|   | |||||||
| @@ -22,18 +22,23 @@ auth: | |||||||
|  |  | ||||||
|   # Basic backend configuration |   # Basic backend configuration | ||||||
|   # To generate bcrypt hashed password from Python, use: |   # To generate bcrypt hashed password from Python, use: | ||||||
|   # python3 -c 'import bcrypt; print(bcrypt.hashpw(b"PASSWORD", bcrypt.gensalt(rounds=15)).decode("ascii"))' |   # python3 -c 'import bcrypt; print(bcrypt.hashpw(b"PASSWORD", bcrypt.gensalt(rounds=12)).decode("ascii"))' | ||||||
|   # |   # | ||||||
|   #basic: |   #basic: | ||||||
|   #  credentials: |   #  credentials: | ||||||
|   #    # Demo user with password "demo" |   #    # Demo user with password "demo" | ||||||
|   #    demo: $2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6 |   #    demo: $2b$10$xuU7XFwmRX2CMgdSaA8rM.4Y8.BtRNzhUedwN0G8tCegDRNUERTCS | ||||||
|  |  | ||||||
|   # LDAP backend configuration |   # LDAP backend configuration | ||||||
|   # |   # | ||||||
|   #ldap: |   #ldap: | ||||||
|   #  uri: ldap://127.0.0.1:389 |   #  uri: ldap://127.0.0.1:389 | ||||||
|   #  userdn: cn=users,dc=example,dc=com |   #  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. | ||||||
|  |   #  aliases: | ||||||
|  |   #    example: demo | ||||||
|  |   # | ||||||
|  |  | ||||||
| ## Stream forwarding ## | ## Stream forwarding ## | ||||||
| # Forward an incoming stream to other servers | # Forward an incoming stream to other servers | ||||||
| @@ -61,6 +66,19 @@ monitoring: | |||||||
|   # To limit access to only localhost, use 127.0.0.1:2112 |   # To limit access to only localhost, use 127.0.0.1:2112 | ||||||
|   #listenAddress: :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 ## | ## SRT server ## | ||||||
| # The SRT server receive incoming stream and can also serve video to clients. | # The SRT server receive incoming stream and can also serve video to clients. | ||||||
| srt: | srt: | ||||||
| @@ -160,17 +178,29 @@ web: | |||||||
|   # |   # | ||||||
|   #widgetURL: "" |   #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 server ## | ||||||
| webrtc: | webrtc: | ||||||
|   # If you disable webrtc module, the web client won't be able to play streams. |   # 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 |   # UDP port range used to stream | ||||||
|   # This range must be opened in your firewall. |   # This range must be opened in your firewall. | ||||||
|   # |   # | ||||||
|   #minPortUDP: 10000 |   #minPortUDP: 10000 | ||||||
|   #maxPortUDP: 10005 |   #maxPortUDP: 11000 | ||||||
|  |  | ||||||
|   # STUN servers, you should host your own Coturn instance |   # STUN servers, you should host your own Coturn instance | ||||||
|   # |   # | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								go.mod
									
									
									
									
									
								
							| @@ -5,11 +5,12 @@ go 1.13 | |||||||
| require ( | require ( | ||||||
| 	github.com/go-ldap/ldap/v3 v3.2.3 | 	github.com/go-ldap/ldap/v3 v3.2.3 | ||||||
| 	github.com/gorilla/websocket v1.4.0 | 	github.com/gorilla/websocket v1.4.0 | ||||||
| 	github.com/haivision/srtgo v0.0.0-20200731151239-e00427ae473a | 	github.com/haivision/srtgo v0.0.0-20201025191851-67964e8f497a | ||||||
| 	github.com/markbates/pkger v0.17.1 | 	github.com/markbates/pkger v0.17.1 | ||||||
| 	github.com/pion/rtp v1.6.0 | 	github.com/pion/rtp v1.6.1 | ||||||
| 	github.com/pion/webrtc/v3 v3.0.0-beta.5 | 	github.com/pion/webrtc/v3 v3.0.0-beta.10 | ||||||
|  | 	github.com/pkg/profile v1.5.0 | ||||||
| 	github.com/prometheus/client_golang v1.7.1 | 	github.com/prometheus/client_golang v1.7.1 | ||||||
| 	github.com/sherifabdlnaby/configuro v0.0.2 | 	github.com/sherifabdlnaby/configuro v0.0.2 | ||||||
| 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 | 	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								go.sum
									
									
									
									
									
								
							| @@ -120,8 +120,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg | |||||||
| github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | ||||||
| github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= | ||||||
| github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | ||||||
| github.com/haivision/srtgo v0.0.0-20200731151239-e00427ae473a h1:JliMkv/mAqM5+QzG6Hkw1XcVl1crU8yIQGnhppMv7s0= | github.com/haivision/srtgo v0.0.0-20201025191851-67964e8f497a h1:54abJQezjMoiP+xMQ3ZQbcDXFjqytAYm/n0EVqrYeXg= | ||||||
| github.com/haivision/srtgo v0.0.0-20200731151239-e00427ae473a/go.mod h1:yVZ4oACfcnUAcxrh+0b6IuIWfkHLK3IAQ99tuuhRx54= | github.com/haivision/srtgo v0.0.0-20201025191851-67964e8f497a/go.mod h1:7izzTiCO3zc9ZIVTFMjxUiYL+kgryFP9rl3bsweqdmc= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||||
| @@ -192,29 +192,30 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 | |||||||
| github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | ||||||
| github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= | github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= | ||||||
| github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= | github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= | ||||||
| github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E= | github.com/pion/dtls/v2 v2.0.3 h1:3qQ0s4+TXD00rsllL8g8KQcxAs+Y/Z6oz618RXX6p14= | ||||||
| github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= | github.com/pion/dtls/v2 v2.0.3/go.mod h1:TUjyL8bf8LH95h81Xj7kATmzMRt29F/4lxpIPj2Xe4Y= | ||||||
| github.com/pion/ice/v2 v2.0.6 h1:7Jf3AX6VIjgO2tGRyT0RGGxkDYOF4m5I5DQzf34IN1Y= | github.com/pion/ice/v2 v2.0.9 h1:oHbiN6Q9tgb8Gfu3I4cbr5mHRE1uqiuFABQ8CbWjIyk= | ||||||
| github.com/pion/ice/v2 v2.0.6/go.mod h1:xOXvVRlQC/B7FPJeJYKY6IepFRAKb3t1un1K9boYaaQ= | github.com/pion/ice/v2 v2.0.9/go.mod h1:NK+o39ynb+N1YSj9fPgWs3vjVcrsWw0KCr/311MqVq8= | ||||||
| github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= | ||||||
| github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= | ||||||
| github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= | github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= | ||||||
| github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= | github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= | ||||||
| github.com/pion/quic v0.1.4 h1:bNz9sCJjlM3GqMdq7Fne57FiWfdyiJ++yHVbuqeoD3Y= | github.com/pion/quic v0.1.4 h1:bNz9sCJjlM3GqMdq7Fne57FiWfdyiJ++yHVbuqeoD3Y= | ||||||
| github.com/pion/quic v0.1.4/go.mod h1:dBhNvkLoQqRwfi6h3Vqj3IcPLgiW7rkZxBbRdp7Vzvk= | github.com/pion/quic v0.1.4/go.mod h1:dBhNvkLoQqRwfi6h3Vqj3IcPLgiW7rkZxBbRdp7Vzvk= | ||||||
| github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= |  | ||||||
| github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= | ||||||
| github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= | ||||||
| github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA= | github.com/pion/rtcp v1.2.4 h1:NT3H5LkUGgaEapvp0HGik+a+CpflRF7KTD7H+o7OWIM= | ||||||
| github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= | github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= | ||||||
| github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= | github.com/pion/rtp v1.6.1 h1:2Y2elcVBrahYnHKN2X7rMHX/r1R4TEBMP1LaVu/wNhk= | ||||||
| github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= | github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= | ||||||
| github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8= | github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8= | ||||||
| github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | ||||||
| github.com/pion/sdp/v3 v3.0.1 h1:we4OyeTT6s+6sxrAKy/LVywskx1dzUQlh4xu3b+iAs0= | github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= | ||||||
| github.com/pion/sdp/v3 v3.0.1/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= | github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | ||||||
| github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= | github.com/pion/sdp/v3 v3.0.2 h1:UNnSPVaMM+Pdu/mR9UvAyyo6zkdYbKeuOooCwZvTl/g= | ||||||
| github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= | github.com/pion/sdp/v3 v3.0.2/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= | ||||||
|  | github.com/pion/srtp v1.5.2 h1:25DmvH+fqKZDqvX64vTwnycVwL9ooJxHF/gkX16bDBY= | ||||||
|  | github.com/pion/srtp v1.5.2/go.mod h1:NiBff/MSxUwMUwx/fRNyD/xGE+dVvf8BOCeXhjCXZ9U= | ||||||
| github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= | ||||||
| github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= | ||||||
| github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= | github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= | ||||||
| @@ -225,12 +226,14 @@ github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk= | |||||||
| github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= | github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= | ||||||
| github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= | github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= | ||||||
| github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= | ||||||
| github.com/pion/webrtc/v3 v3.0.0-beta.5 h1:A1hApeo77Kp2sHWwd53rIHZZm5W9JXEn//5jHj9+fdc= | github.com/pion/webrtc/v3 v3.0.0-beta.10 h1:1aBn9jv/oe4v2Uf47HutWIjg2i2ZP/O7HqpgKPqSuhE= | ||||||
| github.com/pion/webrtc/v3 v3.0.0-beta.5/go.mod h1:HjQaNPq5SdNazGlwiim6/lWBkZD97mPPyDttr7byNA4= | github.com/pion/webrtc/v3 v3.0.0-beta.10/go.mod h1:GlriYYHJ5KkNsCunm3oFDPql4TDTrrNoI9iSWWSnafA= | ||||||
| github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= | ||||||
|  | github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||||
| @@ -349,6 +352,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf | |||||||
| golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
|  | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= | ||||||
|  | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| @@ -384,6 +389,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R | |||||||
| golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||||
| golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= | ||||||
| golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||||
|  | golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw= | ||||||
|  | golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= | ||||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| @@ -416,6 +423,10 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w | |||||||
| golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= | ||||||
| golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c h1:38q6VNPWR010vN82/SB121GujZNIfAUb4YttE2rhGuc= | ||||||
|  | golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= | ||||||
|  | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| @@ -445,6 +456,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T | |||||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= | ||||||
|  | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | ||||||
| google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | ||||||
| google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| package config | package config | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||||
| 	"net" | 	"net" | ||||||
|  |  | ||||||
| 	"github.com/sherifabdlnaby/configuro" | 	"github.com/sherifabdlnaby/configuro" | ||||||
| @@ -23,6 +24,7 @@ type Config struct { | |||||||
| 	Auth       auth.Options | 	Auth       auth.Options | ||||||
| 	Forwarding forwarding.Options | 	Forwarding forwarding.Options | ||||||
| 	Monitoring monitoring.Options | 	Monitoring monitoring.Options | ||||||
|  | 	OME        ovenmediaengine.Options | ||||||
| 	Srt        srt.Options | 	Srt        srt.Options | ||||||
| 	Telnet     telnet.Options | 	Telnet     telnet.Options | ||||||
| 	Transcoder transcoder.Options | 	Transcoder transcoder.Options | ||||||
| @@ -40,6 +42,7 @@ func New() *Config { | |||||||
| 				Credentials: make(map[string]string), | 				Credentials: make(map[string]string), | ||||||
| 			}, | 			}, | ||||||
| 			LDAP: ldap.Options{ | 			LDAP: ldap.Options{ | ||||||
|  | 				Aliases: make(map[string]string), | ||||||
| 				URI:     "ldap://127.0.0.1:389", | 				URI:     "ldap://127.0.0.1:389", | ||||||
| 				UserDn:  "cn=users,dc=example,dc=com", | 				UserDn:  "cn=users,dc=example,dc=com", | ||||||
| 			}, | 			}, | ||||||
| @@ -49,6 +52,11 @@ func New() *Config { | |||||||
| 			Enabled:       true, | 			Enabled:       true, | ||||||
| 			ListenAddress: ":2112", | 			ListenAddress: ":2112", | ||||||
| 		}, | 		}, | ||||||
|  | 		OME: ovenmediaengine.Options{ | ||||||
|  | 			Enabled: true, | ||||||
|  | 			URL:     "ovenmediaengine:1915", | ||||||
|  | 			App:     "play", | ||||||
|  | 		}, | ||||||
| 		Srt: srt.Options{ | 		Srt: srt.Options{ | ||||||
| 			Enabled:       true, | 			Enabled:       true, | ||||||
| 			ListenAddress: ":9710", | 			ListenAddress: ":9710", | ||||||
| @@ -75,10 +83,15 @@ func New() *Config { | |||||||
| 			MapDomainToStream:           make(map[string]string), | 			MapDomainToStream:           make(map[string]string), | ||||||
| 			PlayerPoster:                "/static/img/no_stream.svg", | 			PlayerPoster:                "/static/img/no_stream.svg", | ||||||
| 			ViewersCounterRefreshPeriod: 20000, | 			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{ | 		WebRTC: webrtc.Options{ | ||||||
| 			Enabled:     true, | 			Enabled:     false, | ||||||
| 			MaxPortUDP:  10005, | 			MaxPortUDP:  11000, | ||||||
| 			MinPortUDP:  10000, | 			MinPortUDP:  10000, | ||||||
| 			STUNServers: []string{"stun:stun.l.google.com:19302"}, | 			STUNServers: []string{"stun:stun.l.google.com:19302"}, | ||||||
| 		}, | 		}, | ||||||
| @@ -117,7 +130,7 @@ func Load() (*Config, error) { | |||||||
|  |  | ||||||
| 	// If no credentials register, add demo account with password "demo" | 	// If no credentials register, add demo account with password "demo" | ||||||
| 	if len(cfg.Auth.Basic.Credentials) < 1 { | 	if len(cfg.Auth.Basic.Credentials) < 1 { | ||||||
| 		cfg.Auth.Basic.Credentials["demo"] = "$2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6" | 		cfg.Auth.Basic.Credentials["demo"] = "$2b$10$xuU7XFwmRX2CMgdSaA8rM.4Y8.BtRNzhUedwN0G8tCegDRNUERTCS" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return cfg, nil | 	return cfg, nil | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							| @@ -5,8 +5,10 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||||
| 	"log" | 	"log" | ||||||
|  |  | ||||||
|  | 	"github.com/pkg/profile" | ||||||
| 	"gitlab.crans.org/nounous/ghostream/auth" | 	"gitlab.crans.org/nounous/ghostream/auth" | ||||||
| 	"gitlab.crans.org/nounous/ghostream/internal/config" | 	"gitlab.crans.org/nounous/ghostream/internal/config" | ||||||
| 	"gitlab.crans.org/nounous/ghostream/internal/monitoring" | 	"gitlab.crans.org/nounous/ghostream/internal/monitoring" | ||||||
| @@ -20,6 +22,9 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|  | 	// TODO Don't always profile if not needed | ||||||
|  | 	defer profile.Start().Stop() | ||||||
|  |  | ||||||
| 	// Configure logger | 	// Configure logger | ||||||
| 	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) | 	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) | ||||||
|  |  | ||||||
| @@ -45,9 +50,10 @@ func main() { | |||||||
| 	go transcoder.Init(streams, &cfg.Transcoder) | 	go transcoder.Init(streams, &cfg.Transcoder) | ||||||
| 	go forwarding.Serve(streams, cfg.Forwarding) | 	go forwarding.Serve(streams, cfg.Forwarding) | ||||||
| 	go monitoring.Serve(&cfg.Monitoring) | 	go monitoring.Serve(&cfg.Monitoring) | ||||||
|  | 	go ovenmediaengine.Serve(streams, &cfg.OME) | ||||||
| 	go srt.Serve(streams, authBackend, &cfg.Srt) | 	go srt.Serve(streams, authBackend, &cfg.Srt) | ||||||
| 	go telnet.Serve(streams, &cfg.Telnet) | 	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) | 	go webrtc.Serve(streams, &cfg.WebRTC) | ||||||
|  |  | ||||||
| 	// Wait for routines | 	// Wait for routines | ||||||
|   | |||||||
| @@ -10,12 +10,6 @@ import ( | |||||||
| // Quality holds a specific stream quality. | // Quality holds a specific stream quality. | ||||||
| // It makes packages able to subscribe to an incoming stream. | // It makes packages able to subscribe to an incoming stream. | ||||||
| type Quality struct { | type Quality struct { | ||||||
| 	// Type of the quality |  | ||||||
| 	Name string |  | ||||||
|  |  | ||||||
| 	// Source Stream |  | ||||||
| 	Stream *Stream |  | ||||||
|  |  | ||||||
| 	// Incoming data come from this channel | 	// Incoming data come from this channel | ||||||
| 	Broadcast chan<- []byte | 	Broadcast chan<- []byte | ||||||
|  |  | ||||||
| @@ -33,9 +27,8 @@ type Quality struct { | |||||||
| 	WebRtcRemoteSdp chan webrtc.SessionDescription | 	WebRtcRemoteSdp chan webrtc.SessionDescription | ||||||
| } | } | ||||||
|  |  | ||||||
| func newQuality(name string, stream *Stream) (q *Quality) { | func newQuality() (q *Quality) { | ||||||
| 	q = &Quality{Name: name} | 	q = &Quality{} | ||||||
| 	q.Stream = stream |  | ||||||
| 	broadcast := make(chan []byte, 1024) | 	broadcast := make(chan []byte, 1024) | ||||||
| 	q.Broadcast = broadcast | 	q.Broadcast = broadcast | ||||||
| 	q.outputs = make(map[chan []byte]struct{}) | 	q.outputs = make(map[chan []byte]struct{}) | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ func (s *Stream) CreateQuality(name string) (quality *Quality, err error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s.lockQualities.Lock() | 	s.lockQualities.Lock() | ||||||
| 	quality = newQuality(name, s) | 	quality = newQuality() | ||||||
| 	s.qualities[name] = quality | 	s.qualities[name] = quality | ||||||
| 	s.lockQualities.Unlock() | 	s.lockQualities.Unlock() | ||||||
| 	return quality, nil | 	return quality, nil | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ func Serve(streams *messaging.Streams, cfg Options) { | |||||||
| 		stream, err := streams.Get(name) | 		stream, err := streams.Get(name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Printf("Failed to get stream '%s'", name) | 			log.Printf("Failed to get stream '%s'", name) | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get specific quality | 		// 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, "%S", fmt.Sprintf("%02d", now.Second())) | ||||||
| 		formattedURL = strings.ReplaceAll(formattedURL, "%name", streamName) | 		formattedURL = strings.ReplaceAll(formattedURL, "%name", streamName) | ||||||
|  |  | ||||||
| 		params = append(params, "-f", "flv", "-preset", "ultrafast", "-tune", "zerolatency", | 		params = append(params, "-f", "flv", | ||||||
| 			"-c", "copy", formattedURL) | 			"-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-ar", "44100", formattedURL) | ||||||
| 	} | 	} | ||||||
| 	ffmpeg := exec.Command("ffmpeg", params...) | 	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 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -24,17 +24,6 @@ func handleStreamer(socket *srtgo.SrtSocket, streams *messaging.Streams, name st | |||||||
| 		socket.Close() | 		socket.Close() | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create sub-qualities |  | ||||||
| 	for _, qualityName := range []string{"audio", "480p", "360p", "240p"} { |  | ||||||
| 		_, err := stream.CreateQuality(qualityName) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Printf("Error on quality creating: %s", err) |  | ||||||
| 			socket.Close() |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Printf("New SRT streamer for stream '%s' quality 'source'", name) | 	log.Printf("New SRT streamer for stream '%s' quality 'source'", name) | ||||||
|  |  | ||||||
| 	// Read RTP packets forever and send them to the WebRTC Client | 	// Read RTP packets forever and send them to the WebRTC Client | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| // Package srt serves a SRT server | // Package srt serves a SRT server | ||||||
| package srt | package srt | ||||||
|  |  | ||||||
| // #include <srt/srt.h> |  | ||||||
| import "C" |  | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
| @@ -62,7 +59,7 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) { | |||||||
|  |  | ||||||
| 	for { | 	for { | ||||||
| 		// Wait for new connection | 		// Wait for new connection | ||||||
| 		s, err := sck.Accept() | 		s, _, err := sck.Accept() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			// Something wrong happened | 			// Something wrong happened | ||||||
| 			log.Println(err) | 			log.Println(err) | ||||||
| @@ -73,7 +70,7 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) { | |||||||
| 		// Without this, the SRT buffer might get full before reading it | 		// Without this, the SRT buffer might get full before reading it | ||||||
|  |  | ||||||
| 		// streamid can be "name:password" for streamer or "name" for viewer | 		// streamid can be "name:password" for streamer or "name" for viewer | ||||||
| 		streamID, err := s.GetSockOptString(C.SRTO_STREAMID) | 		streamID, err := s.GetSockOptString(srtgo.SRTO_STREAMID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Print("Failed to get socket streamid") | 			log.Print("Failed to get socket streamid") | ||||||
| 			continue | 			continue | ||||||
|   | |||||||
| @@ -3,7 +3,9 @@ package webrtc | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
|  | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"math/rand" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
|  |  | ||||||
| @@ -14,61 +16,36 @@ import ( | |||||||
|  |  | ||||||
| func ingest(name string, q *messaging.Quality) { | func ingest(name string, q *messaging.Quality) { | ||||||
| 	// Register to get stream | 	// Register to get stream | ||||||
| 	input := make(chan []byte, 1024) | 	videoInput := make(chan []byte, 1024) | ||||||
| 	// FIXME Stream data should already be transcoded | 	q.Register(videoInput) | ||||||
| 	source, _ := q.Stream.GetQuality("source") |  | ||||||
| 	source.Register(input) |  | ||||||
|  |  | ||||||
| 	// FIXME Bad code | 	// FIXME Mux into RTP without having multiple UDP listeners | ||||||
| 	port := 5000 | 	firstPort := int(rand.Int31n(63535)) + 2000 | ||||||
| 	var tracks map[string][]*webrtc.Track |  | ||||||
| 	qualityName := "" | 	// Open UDP listeners for RTP Packets | ||||||
| 	switch q.Name { | 	audioListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: firstPort}) | ||||||
| 	case "audio": | 	if err != nil { | ||||||
| 		port = 5004 | 		log.Printf("Faited to open UDP listener %s", err) | ||||||
| 		tracks = audioTracks | 		return | ||||||
| 		break |  | ||||||
| 	case "source": |  | ||||||
| 		port = 5005 |  | ||||||
| 		tracks = videoTracks |  | ||||||
| 		qualityName = "@source" |  | ||||||
| 		break |  | ||||||
| 	case "480p": |  | ||||||
| 		port = 5006 |  | ||||||
| 		tracks = videoTracks |  | ||||||
| 		qualityName = "@480p" |  | ||||||
| 		break |  | ||||||
| 	case "360p": |  | ||||||
| 		port = 5007 |  | ||||||
| 		tracks = videoTracks |  | ||||||
| 		qualityName = "@360p" |  | ||||||
| 		break |  | ||||||
| 	case "240p": |  | ||||||
| 		port = 5008 |  | ||||||
| 		tracks = videoTracks |  | ||||||
| 		qualityName = "@240p" |  | ||||||
| 		break |  | ||||||
| 	} | 	} | ||||||
|  | 	videoListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: firstPort + 1}) | ||||||
| 	// Open a UDP Listener for RTP Packets |  | ||||||
| 	listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Printf("Faited to open UDP listener %s", err) | 		log.Printf("Faited to open UDP listener %s", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Start ffmpag to convert input to video and audio UDP | 	// Start ffmpag to convert videoInput to video and audio UDP | ||||||
| 	ffmpeg, err := startFFmpeg(q, input) | 	ffmpeg, err := startFFmpeg(videoInput, firstPort) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Printf("Error while starting ffmpeg: %s", err) | 		log.Printf("Error while starting ffmpeg: %s", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Receive stream | 	// Receive video | ||||||
| 	go func() { | 	go func() { | ||||||
| 		inboundRTPPacket := make([]byte, 1500) // UDP MTU | 		inboundRTPPacket := make([]byte, 1500) // UDP MTU | ||||||
| 		for { | 		for { | ||||||
| 			n, _, err := listener.ReadFromUDP(inboundRTPPacket) | 			n, _, err := videoListener.ReadFromUDP(inboundRTPPacket) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Printf("Failed to read from UDP: %s", err) | 				log.Printf("Failed to read from UDP: %s", err) | ||||||
| 				break | 				break | ||||||
| @@ -79,13 +56,49 @@ func ingest(name string, q *messaging.Quality) { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Write RTP srtPacket to all tracks | 			if videoTracks[name] == nil { | ||||||
|  | 				videoTracks[name] = make([]*webrtc.Track, 0) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Write RTP srtPacket to all video tracks | ||||||
| 			// Adapt payload and SSRC to match destination | 			// Adapt payload and SSRC to match destination | ||||||
| 			for _, track := range tracks[name+qualityName] { | 			for _, videoTrack := range videoTracks[name] { | ||||||
| 				packet.Header.PayloadType = track.PayloadType() | 				packet.Header.PayloadType = videoTrack.PayloadType() | ||||||
| 				packet.Header.SSRC = track.SSRC() | 				packet.Header.SSRC = videoTrack.SSRC() | ||||||
| 				if writeErr := track.WriteRTP(packet); writeErr != nil { | 				if writeErr := videoTrack.WriteRTP(packet); writeErr != nil { | ||||||
| 					log.Printf("Failed to write to track: %s", writeErr) | 					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) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			packet := &rtp.Packet{} | ||||||
|  | 			if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil { | ||||||
|  | 				log.Printf("Failed to unmarshal RTP srtPacket: %s", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if audioTracks[name] == nil { | ||||||
|  | 				audioTracks[name] = make([]*webrtc.Track, 0) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Write RTP srtPacket to all audio tracks | ||||||
|  | 			// Adapt payload and SSRC to match destination | ||||||
|  | 			for _, audioTrack := range audioTracks[name] { | ||||||
|  | 				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 | 					continue | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @@ -97,47 +110,24 @@ func ingest(name string, q *messaging.Quality) { | |||||||
| 		log.Printf("Faited to wait for ffmpeg: %s", err) | 		log.Printf("Faited to wait for ffmpeg: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Close UDP listener | 	// Close UDP listeners | ||||||
| 	if err = listener.Close(); err != nil { | 	if err = videoListener.Close(); err != nil { | ||||||
| 		log.Printf("Faited to close UDP listener: %s", err) | 		log.Printf("Faited to close UDP listener: %s", err) | ||||||
| 	} | 	} | ||||||
| 	q.Unregister(input) | 	if err = audioListener.Close(); err != nil { | ||||||
|  | 		log.Printf("Faited to close UDP listener: %s", err) | ||||||
|  | 	} | ||||||
|  | 	q.Unregister(videoInput) | ||||||
| } | } | ||||||
|  |  | ||||||
| func startFFmpeg(q *messaging.Quality, in <-chan []byte) (ffmpeg *exec.Cmd, err error) { | func startFFmpeg(in <-chan []byte, listeningPort int) (ffmpeg *exec.Cmd, err error) { | ||||||
| 	// FIXME Use transcoders to downscale, then remux in RTP | 	ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0", | ||||||
| 	ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0"} | 		// Audio | ||||||
| 	switch q.Name { | 		"-vn", "-c:a", "libopus", "-b:a", "96k", | ||||||
| 	case "audio": | 		"-f", "rtp", fmt.Sprintf("rtp://127.0.0.1:%d", listeningPort), | ||||||
| 		ffmpegArgs = append(ffmpegArgs, "-vn", "-c:a", "libopus", "-b:a", "160k", | 		// Source | ||||||
| 			"-f", "rtp", "rtp://127.0.0.1:5004") | 		"-an", "-c:v", "copy", | ||||||
| 		break | 		"-f", "rtp", fmt.Sprintf("rtp://127.0.0.1:%d", listeningPort+1)} | ||||||
| 	case "source": |  | ||||||
| 		ffmpegArgs = append(ffmpegArgs, "-an", "-c:v", "copy", |  | ||||||
| 			"-f", "rtp", "rtp://127.0.0.1:5005") |  | ||||||
| 		break |  | ||||||
| 	case "480p": |  | ||||||
| 		ffmpegArgs = append(ffmpegArgs, |  | ||||||
| 			"-an", "-c:v", "libx264", "-b:v", "1200k", "-maxrate", "2000k", "-bufsize", "3000k", |  | ||||||
| 			"-preset", "ultrafast", "-profile", "main", "-tune", "zerolatency", |  | ||||||
| 			"-vf", "scale=854:480", |  | ||||||
| 			"-f", "rtp", "rtp://127.0.0.1:5006") |  | ||||||
| 		break |  | ||||||
| 	case "360p": |  | ||||||
| 		ffmpegArgs = append(ffmpegArgs, |  | ||||||
| 			"-an", "-c:v", "libx264", "-b:v", "800k", "-maxrate", "1200k", "-bufsize", "1500k", |  | ||||||
| 			"-preset", "ultrafast", "-profile", "main", "-tune", "zerolatency", |  | ||||||
| 			"-vf", "scale=480:360", |  | ||||||
| 			"-f", "rtp", "rtp://127.0.0.1:5007") |  | ||||||
| 		break |  | ||||||
| 	case "240p": |  | ||||||
| 		ffmpegArgs = append(ffmpegArgs, |  | ||||||
| 			"-an", "-c:v", "libx264", "-b:v", "500k", "-maxrate", "800k", "-bufsize", "1000k", |  | ||||||
| 			"-preset", "ultrafast", "-profile", "main", "-tune", "zerolatency", |  | ||||||
| 			"-vf", "scale=360:240", |  | ||||||
| 			"-f", "rtp", "rtp://127.0.0.1:5008") |  | ||||||
| 		break |  | ||||||
| 	} |  | ||||||
| 	ffmpeg = exec.Command("ffmpeg", ffmpegArgs...) | 	ffmpeg = exec.Command("ffmpeg", ffmpegArgs...) | ||||||
|  |  | ||||||
| 	// Handle errors output | 	// Handle errors output | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ func removeTrack(tracks []*webrtc.Track, track *webrtc.Track) []*webrtc.Track { | |||||||
|  |  | ||||||
| // GetNumberConnectedSessions get the number of currently connected clients | // GetNumberConnectedSessions get the number of currently connected clients | ||||||
| func GetNumberConnectedSessions(streamID string) int { | func GetNumberConnectedSessions(streamID string) int { | ||||||
| 	return len(audioTracks[streamID]) | 	return len(videoTracks[streamID]) | ||||||
| } | } | ||||||
|  |  | ||||||
| // newPeerHandler is called when server receive a new session description | // newPeerHandler is called when server receive a new session description | ||||||
| @@ -117,20 +117,21 @@ func newPeerHandler(name string, localSdpChan chan webrtc.SessionDescription, re | |||||||
| 		quality = split[1] | 		quality = split[1] | ||||||
| 	} | 	} | ||||||
| 	log.Printf("New WebRTC session for stream %s, quality %s", streamID, quality) | 	log.Printf("New WebRTC session for stream %s, quality %s", streamID, quality) | ||||||
|  | 	// TODO Consider the quality | ||||||
|  |  | ||||||
| 	// Set the handler for ICE connection state | 	// Set the handler for ICE connection state | ||||||
| 	// This will notify you when the peer has connected/disconnected | 	// This will notify you when the peer has connected/disconnected | ||||||
| 	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { | 	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { | ||||||
| 		log.Printf("Connection State has changed %s \n", connectionState.String()) | 		log.Printf("Connection State has changed %s \n", connectionState.String()) | ||||||
| 		if videoTracks[streamID+"@"+quality] == nil { | 		if videoTracks[streamID] == nil { | ||||||
| 			videoTracks[streamID+"@"+quality] = make([]*webrtc.Track, 0, 1) | 			videoTracks[streamID] = make([]*webrtc.Track, 0, 1) | ||||||
| 		} | 		} | ||||||
| 		if audioTracks[streamID] == nil { | 		if audioTracks[streamID] == nil { | ||||||
| 			audioTracks[streamID] = make([]*webrtc.Track, 0, 1) | 			audioTracks[streamID] = make([]*webrtc.Track, 0, 1) | ||||||
| 		} | 		} | ||||||
| 		if connectionState == webrtc.ICEConnectionStateConnected { | 		if connectionState == webrtc.ICEConnectionStateConnected { | ||||||
| 			// Register tracks | 			// Register tracks | ||||||
| 			videoTracks[streamID+"@"+quality] = append(videoTracks[streamID+"@"+quality], videoTrack) | 			videoTracks[streamID] = append(videoTracks[streamID], videoTrack) | ||||||
| 			audioTracks[streamID] = append(audioTracks[streamID], audioTrack) | 			audioTracks[streamID] = append(audioTracks[streamID], audioTrack) | ||||||
| 			monitoring.WebRTCConnectedSessions.Inc() | 			monitoring.WebRTCConnectedSessions.Inc() | ||||||
| 		} else if connectionState == webrtc.ICEConnectionStateDisconnected { | 		} else if connectionState == webrtc.ICEConnectionStateDisconnected { | ||||||
| @@ -204,7 +205,7 @@ func Serve(streams *messaging.Streams, cfg *Options) { | |||||||
|  |  | ||||||
| 		// Get specific quality | 		// Get specific quality | ||||||
| 		// FIXME: make it possible to forward other qualities | 		// FIXME: make it possible to forward other qualities | ||||||
| 		for _, qualityName := range []string{"source", "audio", "480p", "360p", "240p"} { | 		qualityName := "source" | ||||||
| 		quality, err := stream.GetQuality(qualityName) | 		quality, err := stream.GetQuality(qualityName) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Printf("Failed to get quality '%s'", qualityName) | 			log.Printf("Failed to get quality '%s'", qualityName) | ||||||
| @@ -216,7 +217,6 @@ func Serve(streams *messaging.Streams, cfg *Options) { | |||||||
| 		go listenSdp(name, quality.WebRtcLocalSdp, quality.WebRtcRemoteSdp, cfg) | 		go listenSdp(name, quality.WebRtcLocalSdp, quality.WebRtcRemoteSdp, cfg) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| func listenSdp(name string, localSdp, remoteSdp chan webrtc.SessionDescription, cfg *Options) { | func listenSdp(name string, localSdp, remoteSdp chan webrtc.SessionDescription, cfg *Options) { | ||||||
| 	// Handle new connections | 	// Handle new connections | ||||||
|   | |||||||
| @@ -10,15 +10,21 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"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/ovenmediaengine" | ||||||
| 	"gitlab.crans.org/nounous/ghostream/stream/webrtc" | 	"gitlab.crans.org/nounous/ghostream/stream/webrtc" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	// Precompile regex | 	// Precompile regex | ||||||
| 	validPath = regexp.MustCompile("^/[a-z0-9@_-]*$") | 	validPath = regexp.MustCompile("^/[a-z0-9@_-]*$") | ||||||
|  |  | ||||||
|  | 	counterMutex     = new(sync.Mutex) | ||||||
|  | 	connectedClients = make(map[string]map[string]int64) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Handle site index and viewer pages | // Handle site index and viewer pages | ||||||
| @@ -61,7 +67,8 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		Cfg       *Options | 		Cfg       *Options | ||||||
| 		Path      string | 		Path      string | ||||||
| 		WidgetURL 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 | 	// Load widget is user does not disable it with ?nowidget | ||||||
| 	if _, ok := r.URL.Query()["nowidget"]; !ok { | 	if _, ok := r.URL.Query()["nowidget"]; !ok { | ||||||
| @@ -88,14 +95,43 @@ func staticHandler() http.Handler { | |||||||
| } | } | ||||||
|  |  | ||||||
| func statisticsHandler(w http.ResponseWriter, r *http.Request) { | 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] | 	name := strings.SplitN(strings.Replace(r.URL.Path[7:], "/", "", -1), "@", 2)[0] | ||||||
| 	userCount := 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 | 	// Get requested stream | ||||||
| 	stream, err := streams.Get(name) | 	stream, err := streams.Get(name) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		userCount = stream.ClientCount() | 		userCount = stream.ClientCount() | ||||||
| 		userCount += webrtc.GetNumberConnectedSessions(name) | 		userCount += webrtc.GetNumberConnectedSessions(name) | ||||||
|  | 		userCount += len(connectedClients[name]) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Display connected users statistics | 	// Display connected users statistics | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ export class ViewerCounter { | |||||||
|     constructor(element, streamName) { |     constructor(element, streamName) { | ||||||
|         this.element = element; |         this.element = element; | ||||||
|         this.url = "/_stats/" + streamName; |         this.url = "/_stats/" + streamName; | ||||||
|  |         this.uid = Math.floor(1e19 * Math.random()).toString(16); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -21,7 +22,7 @@ export class ViewerCounter { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     refreshViewersCounter() { |     refreshViewersCounter() { | ||||||
|         fetch(this.url) |         fetch(this.url + "?uid=" + this.uid) | ||||||
|             .then(response => response.json()) |             .then(response => response.json()) | ||||||
|             .then((data) => this.element.innerText = data.ConnectedViewers) |             .then((data) => this.element.innerText = data.ConnectedViewers) | ||||||
|             .catch(console.log); |             .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; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
| @@ -14,7 +14,7 @@ export function initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) | |||||||
|     const viewer = document.getElementById("viewer"); |     const viewer = document.getElementById("viewer"); | ||||||
|  |  | ||||||
|     // Default quality |     // Default quality | ||||||
|     let quality = "240p"; |     let quality = "source"; | ||||||
|  |  | ||||||
|     // Create WebSocket and WebRTC |     // Create WebSocket and WebRTC | ||||||
|     const websocket = new GsWebSocket(); |     const websocket = new GsWebSocket(); | ||||||
|   | |||||||
							
								
								
									
										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> |   </p> | ||||||
|  |  | ||||||
|   <h2>Comment je diffuse ?</h2> |   <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> |   <h3>Avec Open Broadcaster Software</h3> | ||||||
|   <p> |   <p> | ||||||
| @@ -21,11 +25,7 @@ | |||||||
|   <ul> |   <ul> | ||||||
|     <li> |     <li> | ||||||
|       <b>Serveur :</b> |       <b>Serveur :</b> | ||||||
|       <code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}</code>, |       <code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?IDENTIFIANT:MOT_DE_PASSE</code>, | ||||||
|     </li> |  | ||||||
|     <li> |  | ||||||
|       <b>Clé de stream :</b> |  | ||||||
|       <code>IDENTIFIANT:MOT_DE_PASSE</code> |  | ||||||
|       avec <code>IDENTIFIANT</code> et <code>MOT_DE_PASSE</code> |       avec <code>IDENTIFIANT</code> et <code>MOT_DE_PASSE</code> | ||||||
|       vos identifiants. |       vos identifiants. | ||||||
|     </li> |     </li> | ||||||
| @@ -46,29 +46,104 @@ | |||||||
|   <p> |   <p> | ||||||
|     <code> |     <code> | ||||||
|       {{/* FIXME replace with good SRT params */}} |       {{/* 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 |       -acodec aac -strict -2 -f flv | ||||||
|       srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT:MOT_DE_PASSE |       srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT:MOT_DE_PASSE | ||||||
|     </code> |     </code> | ||||||
|   </p> |   </p> | ||||||
|  |  | ||||||
|  |   <h2>Comment lire un flux depuis un lecteur externe ?</h2> | ||||||
|  |   <p> | ||||||
|  |     À l'heure actuelle, la plupart des lecteurs vidéos ne supportent | ||||||
|  |     pas le protocole SRT, ou le supportent mal. Un travail est en | ||||||
|  |     cours pour les rendre un maximum compatibles. Liste non exhaustive | ||||||
|  |     des lecteurs vidéos testés : | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <h3>FFPlay</h3> | ||||||
|  |   <p> | ||||||
|  |     Si FFMpeg est installé sur votre machine, il est accompagné d'un | ||||||
|  |     lecteur vidéo nommé <code>ffplay</code>. Si FFMpeg est compilé | ||||||
|  |     avec le support de SRT (c'est le cas sur la plupart des distributions, | ||||||
|  |     sauf cas ci-dessous), il vous suffira d'exécuter : | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     <code> | ||||||
|  |       ffplay -fflags nobuffer srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT | ||||||
|  |     </code> | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <h3>MPV</h3> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     MPV supporte officiellement SRT depuis le 16 octobre 2020. | ||||||
|  |     Néanmoins, la version stable de MPV est beaucoup plus vieille. | ||||||
|  |     Vous devez alors utiliser une version de développement pour | ||||||
|  |     pouvoir lire un flux SRT depuis MPV. L'installation se fait | ||||||
|  |     depuis <a href="https://mpv.io/installation/"> cette page</a>. | ||||||
|  |     Sous Arch Linux, il vous suffit de récupérer le paquet | ||||||
|  |     <code>mpv-git</code> dans l'AUR. Pour lire le flux, il suffit | ||||||
|  |     d'exécuter : | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     <code> | ||||||
|  |       mpv srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT | ||||||
|  |     </code> | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <h3>VLC Media Player</h3> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     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. | ||||||
|  |     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> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     <code> | ||||||
|  |       vlc srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT | ||||||
|  |     </code> | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <p> | ||||||
|  |     Ou bien d'aller dans Média -> Ouvrir un flux réseau et d'entrer l'URL | ||||||
|  |     <code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT</code>. | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|  |   <h3>Le protocole n'existe pas ou n'est pas supporté.</h3> | ||||||
|  |   <p> | ||||||
|  |     La technologie SRT est très récente et n'est pas supportée par | ||||||
|  |     les dépôts stables. Ainsi, si vous avez Ubuntu ≤ 20.04 ou | ||||||
|  |     Debian ≤ Buster, vous ne pourrez pas utiliser de lecteur vidéo | ||||||
|  |     ni même diffuser avec votre machine. Vous devrez vous mettre à | ||||||
|  |     jour vers Ubuntu 20.10 ou Debian Bullseye. | ||||||
|  |   </p> | ||||||
|  |  | ||||||
|   <h2>Mentions légales</h2> |   <h2>Mentions légales</h2> | ||||||
|   <p> |   <p> | ||||||
|     Le service de diffusion vidéo du Crans est un service d'hébergement |     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. |     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, |     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é. |     l'identification des auteurs du contenu diffusé. | ||||||
|     Ce service est hébergé par l'association Crans, au |     Ce service est hébergé par {{.Cfg.LegalMentionsEntity}}, au | ||||||
|     61 Avenue du Président Wilson, 94235 Cachan Cedex, France. |     {{.Cfg.LegalMentionsAddress}}. | ||||||
|   </p> |   </p> | ||||||
|   <p> |   <p> | ||||||
|     <b>En cas de réclamation sur le contenu diffusé</b>, |     <b>En cas de réclamation sur le contenu diffusé</b>, | ||||||
|     la loi vous autorise à contacter directement l'hébergeur à |     la loi vous autorise à contacter directement l'hébergeur à | ||||||
|     l'adresse suivante : |     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 |     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> |   </p> | ||||||
| </div> | </div> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -6,14 +6,14 @@ | |||||||
|  |  | ||||||
|     <!-- Links and settings under video --> |     <!-- Links and settings under video --> | ||||||
|     <div class="controls"> |     <div class="controls"> | ||||||
|       <span class="control-quality"> |       <!-- <span class="control-quality"> | ||||||
|         <select id="quality"> |         <select id="quality"> | ||||||
|           <option value="240p">Source</option> |           <option value="source">Source</option> | ||||||
|  |           <option value="720p">720p</option> | ||||||
|           <option value="480p">480p</option> |           <option value="480p">480p</option> | ||||||
|           <option value="360p">360p</option> |  | ||||||
|           <option value="240p">240p</option> |           <option value="240p">240p</option> | ||||||
|         </select>   |         </select>   | ||||||
|       </span> |       </span> --> | ||||||
|       <code class="control-srt-link">srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid={{.Path}}</code> |       <code class="control-srt-link">srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid={{.Path}}</code> | ||||||
|       <span class="control-viewers" id="connected-people">0</span> |       <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"> |       <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}} |   {{end}} | ||||||
| </div> | </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"> | <script type="module"> | ||||||
|  |   {{if .OMECfg.Enabled}} | ||||||
|  |     import { initViewerPage } from "/static/js/ovenplayer.js"; | ||||||
|  |   {{else}} | ||||||
|     import { initViewerPage } from "/static/js/viewer.js"; |     import { initViewerPage } from "/static/js/viewer.js"; | ||||||
|  |   {{end}} | ||||||
|  |  | ||||||
|   // Some variables that need to be fixed by web page |   // Some variables that need to be fixed by web page | ||||||
|   const viewersCounterRefreshPeriod = Number("{{.Cfg.ViewersCounterRefreshPeriod}}"); |   const viewersCounterRefreshPeriod = Number("{{.Cfg.ViewersCounterRefreshPeriod}}"); | ||||||
| @@ -45,6 +55,10 @@ | |||||||
|     "{{$value}}", |     "{{$value}}", | ||||||
|     {{end}} |     {{end}} | ||||||
|   ] |   ] | ||||||
|  |   {{if .OMECfg.Enabled}} | ||||||
|  |     initViewerPage(stream, {{.OMECfg.App}}, viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}}) | ||||||
|  |   {{else}} | ||||||
|     initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) |     initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) | ||||||
|  |   {{end}} | ||||||
| </script> | </script> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								web/web.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								web/web.go
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ | |||||||
| package web | package web | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"log" | 	"log" | ||||||
| @@ -27,11 +28,17 @@ type Options struct { | |||||||
| 	STUNServers                 []string | 	STUNServers                 []string | ||||||
| 	ViewersCounterRefreshPeriod int | 	ViewersCounterRefreshPeriod int | ||||||
| 	WidgetURL                   string | 	WidgetURL                   string | ||||||
|  | 	LegalMentionsEntity         string | ||||||
|  | 	LegalMentionsAddress        string | ||||||
|  | 	LegalMentionsFullAddress    []string | ||||||
|  | 	LegalMentionsEmail          string | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	cfg *Options | 	cfg *Options | ||||||
|  |  | ||||||
|  | 	omeCfg *ovenmediaengine.Options | ||||||
|  |  | ||||||
| 	// Preload templates | 	// Preload templates | ||||||
| 	templates *template.Template | 	templates *template.Template | ||||||
|  |  | ||||||
| @@ -70,9 +77,10 @@ func loadTemplates() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Serve HTTP server | // Serve HTTP server | ||||||
| func Serve(s *messaging.Streams, c *Options) { | func Serve(s *messaging.Streams, c *Options, ome *ovenmediaengine.Options) { | ||||||
| 	streams = s | 	streams = s | ||||||
| 	cfg = c | 	cfg = c | ||||||
|  | 	omeCfg = ome | ||||||
|  |  | ||||||
| 	if !cfg.Enabled { | 	if !cfg.Enabled { | ||||||
| 		// Web server is not enabled, ignore | 		// Web server is not enabled, ignore | ||||||
|   | |||||||
| @@ -36,7 +36,8 @@ func websocketHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		err = conn.ReadJSON(c) | 		err = conn.ReadJSON(c) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Printf("Failed to receive client description: %s", err) | 			log.Printf("Failed to receive client description: %s", err) | ||||||
| 			continue | 			_ = conn.Close() | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get requested stream | 		// Get requested stream | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user