1
0
mirror of https://gitlab.crans.org/nounous/ghostream.git synced 2025-07-01 16:51:16 +02:00

44 Commits

Author SHA1 Message Date
3493ba5e2f README: add better alternative 2023-08-20 18:50:21 +02:00
f7cf187bac Ignore stream name case 2021-02-25 17:45:36 +01:00
dc594d091c Ignore stream name case 2021-02-25 17:38:09 +01:00
a429216735 Allow uppercase letters in stream name 2021-02-25 17:23:11 +01:00
e6fd4f6352 Use streamid id option, wrong documentation 2021-01-12 00:06:47 +01:00
34652f8f3e I am an idiot, don't let only people with the *wrong* password stream 2021-01-08 23:05:01 +01:00
79f52ed880 Log the LDAP connection 2021-01-08 22:56:19 +01:00
ee16bf9e21 Alias is not properly replaced 2021-01-08 22:46:14 +01:00
e47aefd6df Replace the name of the stream if using an alias 2021-01-08 22:23:33 +01:00
7e0ee7aba5 Match aliases as groups 2021-01-03 05:07:25 +01:00
8d2adad509 Avoid infinite loop 2020-12-06 13:41:17 +01:00
0035c63c22 Add aliases auth support if the authentication method is LDAP 2020-12-06 13:36:24 +01:00
849196b4cb Add DASH player source for more compatibility 2020-11-20 03:26:10 +01:00
205c4b526c Upgrade ovenplayer, add HTML5 provider 2020-11-20 02:46:25 +01:00
1d117ea480 Config on legal mentions 2020-11-12 01:42:28 +01:00
45cb61e436 Merge branch 'ovenmediaengine' into 'dev'
OvenMediaEngine

See merge request nounous/ghostream!8
2020-11-09 21:53:44 +01:00
7e4adb475a Lock counter map 2020-11-09 18:11:42 +01:00
d1c4f81f4e Avoid concurrent map read/write 2020-11-09 18:03:15 +01:00
b2104a0cb7 Get stats by a bad but functionnal way 2020-11-09 17:57:55 +01:00
6ca354f44f Web handler adapts its configuration wherever OME support is enabled or not 2020-11-09 17:31:58 +01:00
a20c6202fd Export config 2020-11-09 17:28:56 +01:00
b52f377b6b OME has its dedicated FFMPEG instance, in order to keep modularity 2020-11-09 17:26:58 +01:00
3d8ba0623d Use config file for OME broadcasting 2020-11-09 17:18:37 +01:00
cfcde6f530 Fix full screen mode 2020-11-09 16:53:35 +01:00
28ef6a5526 Fix connection indicator 2020-11-09 16:48:05 +01:00
5ad8a69c4c A static file is not a template 2020-11-09 16:40:43 +01:00
d334556d2b Debugging player 2020-11-09 16:32:35 +01:00
9625cba5e1 Register keyboard events 2020-11-09 16:24:39 +01:00
e74acf04f7 Update connection indicator 2020-11-09 16:23:13 +01:00
2085d13c0d Export ovenplayer in a separate file 2020-11-09 16:15:23 +01:00
85a5606291 Investigate of why I don't have any audio 2020-11-09 15:57:30 +01:00
33f86a0742 Investigate of why I don't have any audio 2020-11-09 15:55:44 +01:00
11d89c6950 Encode audio with opus codec 2020-11-09 15:48:30 +01:00
c9a2d5b359 Add ovenmediaengine in example config 2020-11-09 15:47:42 +01:00
ee927c5b8f Wrong video element id 2020-11-09 14:41:44 +01:00
955364a5fc Install OvenMediaEngine 2020-11-09 13:48:44 +01:00
fc3c0f606c Reduce bcrypt rounds for demo password to 10 to displaying the bcrypt process in the CPU profiling 2020-11-07 15:03:42 +01:00
e1bdae5380 Add profiling during debugging 2020-11-07 15:02:09 +01:00
26e7182085 Close websocket handler on error 2020-11-07 15:00:33 +01:00
59c47ca3e0 5 UDP ports are not enough 2020-11-07 15:00:06 +01:00
8eea7d6e4f Upgrade pion 2020-11-07 14:58:15 +01:00
f394af9257 Update main page 2020-10-29 13:43:36 +01:00
5b40aa886f Use random UDP ports to able to have multiple concurrent streams 2020-10-29 13:11:30 +01:00
9fc3d37e72 Update srtgo to don't depend anymore on C library 2020-10-29 13:04:13 +01:00
38 changed files with 726 additions and 93 deletions

View File

@ -12,6 +12,6 @@ FROM alpine:3.12
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/community/ ffmpeg libsrt
COPY --from=build_base /code/out/ghostream /app/ghostream
WORKDIR /app
# 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-10005 (UDP) for WebRTC
EXPOSE 2112 8023 8080 9710/udp 10000-10005/udp
# 2112 for monitoring, 8023 for Telnet, 8080 for Web, 9710 for SRT, 10000-11000 (UDP) for WebRTC
EXPOSE 2112 8023 8080 9710/udp 10000-11000/udp
CMD ["/app/ghostream"]

View File

@ -11,6 +11,13 @@
This project was developped at [Cr@ns](https://crans.org/) to stream events.
> **Note**
> *This project is no longer maintained!*
>
> As an alternative, you should try [Galène](https://galene.org/) which supports [WebRTC-HTTP ingestion protocol (WHIP)](https://datatracker.ietf.org/doc/draft-ietf-wish-whip/) for low-latency streaming.
> OBS Studio introduced WHIP output in version 30.0.
> Galène supports WHIP since Galène 0.8.
Features:
- WebRTC playback with a lightweight web interface.

View File

@ -20,7 +20,7 @@ type Options struct {
// Backend to log user in
type Backend interface {
Login(string, string) (bool, error)
Login(string, string) (bool, string, error)
Close()
}

View File

@ -23,15 +23,15 @@ type Basic struct {
// Login hashs password and compare
// Returns (true, nil) if success
func (a Basic) Login(username string, password string) (bool, error) {
func (a Basic) Login(username string, password string) (bool, string, error) {
hash, ok := a.Cfg.Credentials[username]
if !ok {
return false, errors.New("user not found in credentials")
return false, "", errors.New("user not found in credentials")
}
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
// Login succeeded if no error
return err == nil, err
return err == nil, username, err
}
// Close has no connection to close

View File

@ -6,23 +6,23 @@ import (
func TestBasicLogin(t *testing.T) {
basicCredentials := make(map[string]string)
basicCredentials["demo"] = "$2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6"
basicCredentials["demo"] = "$2b$10$xuU7XFwmRX2CMgdSaA8rM.4Y8.BtRNzhUedwN0G8tCegDRNUERTCS"
// Test good credentials
backend, _ := New(&Options{Credentials: basicCredentials})
ok, err := backend.Login("demo", "demo")
ok, _, err := backend.Login("demo", "demo")
if !ok {
t.Error("Error while logging with the basic authentication:", err)
}
// Test bad username
ok, err = backend.Login("baduser", "demo")
ok, _, err = backend.Login("baduser", "demo")
if ok {
t.Error("Authentification failed to fail:", err)
}
// Test bad password
ok, err = backend.Login("demo", "badpass")
ok, _, err = backend.Login("demo", "badpass")
if ok {
t.Error("Authentification failed to fail:", err)
}

View File

@ -3,10 +3,13 @@ package ldap
import (
"github.com/go-ldap/ldap/v3"
"log"
"strings"
)
// Options holds package configuration
type Options struct {
Aliases map[string]map[string]string
URI string
UserDn string
}
@ -19,13 +22,37 @@ type LDAP struct {
// Login tries to bind to LDAP
// Returns (true, nil) if success
func (a LDAP) Login(username string, password string) (bool, error) {
func (a LDAP) Login(username string, password string) (bool, string, error) {
aliasSplit := strings.SplitN(username, "__", 2)
potentialUsernames := []string{username}
if len(aliasSplit) == 2 {
alias := aliasSplit[0]
trueUsername := aliasSplit[1]
// Resolve stream alias if necessary
if aliases, ok := a.Cfg.Aliases[alias]; ok {
if _, ok := aliases[trueUsername]; ok {
log.Printf("[LDAP] Use stream alias %s for username %s", alias, trueUsername)
potentialUsernames = append(potentialUsernames, trueUsername)
}
}
}
var err error = nil
for _, username := range potentialUsernames {
// Try to bind as user
bindDn := "cn=" + username + "," + a.Cfg.UserDn
err := a.Conn.Bind(bindDn, password)
log.Printf("[LDAP] Logging to %s...", bindDn)
err = a.Conn.Bind(bindDn, password)
if err == nil {
// Login succeeded if no error
return err == nil, err
return true, aliasSplit[0], nil
}
}
log.Printf("[LDAP] Logging failed: %s", err)
// Unable to log in
return err == nil, "", err
}
// Close LDAP connection

118
docs/Server-docker.xml Normal file
View 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>

View File

@ -26,11 +26,10 @@ services:
- "--certificatesResolvers.mytlschallenge.acme.httpChallenge.entryPoint=web"
ghostream:
build: ..
build: https://gitlab.crans.org/nounous/ghostream.git
restart: always
ports:
- 9710:9710/udp
- 10000-10005:10000-10005/udp
volumes:
- ./ghostream_data:/etc/ghostream:ro
labels:
@ -40,3 +39,30 @@ services:
- "traefik.http.routers.ghostream.tls.certresolver=mytlschallenge"
- "traefik.http.routers.ghostream.service=ghostream"
- "traefik.http.services.ghostream.loadbalancer.server.port=8080"
ovenmediaengine:
image: airensoft/ovenmediaengine:0.10.8
restart: always
ports:
# WebRTC ICE
- 10006-10010:10006-10010/udp
volumes:
- ./ovenmediaengine_data/conf/Server-docker.xml:/opt/ovenmediaengine/bin/origin_conf/Server.xml:ro
labels:
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.ovenmediaengine.rule=Host(`stream.example.com`) && PathPrefix(`/play/`)"
- "traefik.http.routers.ovenmediaengine.priority=101"
- "traefik.http.routers.ovenmediaengine.entrypoints=websecure"
- "traefik.http.routers.ovenmediaengine.tls.certresolver=mytlschallenge"
- "traefik.http.services.ovenmediaengine.loadbalancer.server.port=3333"
- "traefik.http.routers.ovenmediaengine.service=ovenmediaengine"
- "traefik.http.routers.ovenmediaengine.middlewares=sslheader"
- "traefik.http.routers.ovenmediaengine-hls.rule=Host(`stream.example.com`) && Path(`/play/{app_name:.*}/{filename:.*}.{ext:(m3u8|mpd|ts)}`)"
- "traefik.http.routers.ovenmediaengine-hls.priority=102"
- "traefik.http.routers.ovenmediaengine-hls.entrypoints=websecure"
- "traefik.http.routers.ovenmediaengine-hls.tls.certresolver=mytlschallenge"
- "traefik.http.services.ovenmediaengine-hls.loadbalancer.server.port=80"
- "traefik.http.routers.ovenmediaengine-hls.service=ovenmediaengine-hls"
- "traefik.http.routers.ovenmediaengine-hls.middlewares=sslheader"

View File

@ -22,18 +22,25 @@ auth:
# Basic backend configuration
# 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:
# credentials:
# # Demo user with password "demo"
# demo: $2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6
# demo: $2b$10$xuU7XFwmRX2CMgdSaA8rM.4Y8.BtRNzhUedwN0G8tCegDRNUERTCS
# LDAP backend configuration
#
#ldap:
# uri: ldap://127.0.0.1:389
# userdn: cn=users,dc=example,dc=com
#
# # You can define aliases, to stream on stream.example.com/example with the credentials of the demo account.
# # You will have to use the streamid example__demo:password
# aliases:
# example:
# demo: ignored
#
## Stream forwarding ##
# Forward an incoming stream to other servers
@ -61,6 +68,19 @@ monitoring:
# To limit access to only localhost, use 127.0.0.1:2112
#listenAddress: :2112
## OvenMediaEngine ##
# Send the stream data to OvenMediaEngine to handle properly the web client
ome:
# If you disable OME module, the laggy webrtc client will be used.
#
#enabled: true
#
# The URL where OME listens RTMP, without the prefix.
#url: ovenmediaengine:1915
#
# The OME app where OME is waiting for the data of Ghostream.
#app: play
## SRT server ##
# The SRT server receive incoming stream and can also serve video to clients.
srt:
@ -160,17 +180,29 @@ web:
#
#widgetURL: ""
# IMPORTANT, CHANGE THIS
# You need to declare which entity you are and to specify an address to claim some content.
legalMentionsEntity: "l'association Crans"
legalMentionsAddress: "61 Avenue du Président Wilson, 94235 Cachan Cedex, France"
legalMentionsFullAddress:
- Association Cr@ns - ENS Paris-Saclay
- Notification de Contenus Illicites
- 4, avenue des Sciences
- 91190 Gif-sur-Yvette
- France
legalMentionsEmail: "bureau[at]crans.org"
## WebRTC server ##
webrtc:
# If you disable webrtc module, the web client won't be able to play streams.
#
#enabled: true
#enabled: false
# UDP port range used to stream
# This range must be opened in your firewall.
#
#minPortUDP: 10000
#maxPortUDP: 10005
#maxPortUDP: 11000
# STUN servers, you should host your own Coturn instance
#

9
go.mod
View File

@ -5,11 +5,12 @@ go 1.13
require (
github.com/go-ldap/ldap/v3 v3.2.3
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/pion/rtp v1.6.0
github.com/pion/webrtc/v3 v3.0.0-beta.5
github.com/pion/rtp v1.6.1
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/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
View File

@ -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/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/haivision/srtgo v0.0.0-20200731151239-e00427ae473a h1:JliMkv/mAqM5+QzG6Hkw1XcVl1crU8yIQGnhppMv7s0=
github.com/haivision/srtgo v0.0.0-20200731151239-e00427ae473a/go.mod h1:yVZ4oACfcnUAcxrh+0b6IuIWfkHLK3IAQ99tuuhRx54=
github.com/haivision/srtgo v0.0.0-20201025191851-67964e8f497a h1:54abJQezjMoiP+xMQ3ZQbcDXFjqytAYm/n0EVqrYeXg=
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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
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.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/ice/v2 v2.0.6 h1:7Jf3AX6VIjgO2tGRyT0RGGxkDYOF4m5I5DQzf34IN1Y=
github.com/pion/ice/v2 v2.0.6/go.mod h1:xOXvVRlQC/B7FPJeJYKY6IepFRAKb3t1un1K9boYaaQ=
github.com/pion/dtls/v2 v2.0.3 h1:3qQ0s4+TXD00rsllL8g8KQcxAs+Y/Z6oz618RXX6p14=
github.com/pion/dtls/v2 v2.0.3/go.mod h1:TUjyL8bf8LH95h81Xj7kATmzMRt29F/4lxpIPj2Xe4Y=
github.com/pion/ice/v2 v2.0.9 h1:oHbiN6Q9tgb8Gfu3I4cbr5mHRE1uqiuFABQ8CbWjIyk=
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/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
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/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/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/rtcp v1.2.4 h1:NT3H5LkUGgaEapvp0HGik+a+CpflRF7KTD7H+o7OWIM=
github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtp v1.6.1 h1:2Y2elcVBrahYnHKN2X7rMHX/r1R4TEBMP1LaVu/wNhk=
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/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sdp/v3 v3.0.1 h1:we4OyeTT6s+6sxrAKy/LVywskx1dzUQlh4xu3b+iAs0=
github.com/pion/sdp/v3 v3.0.1/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE=
github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sdp/v3 v3.0.2 h1:UNnSPVaMM+Pdu/mR9UvAyyo6zkdYbKeuOooCwZvTl/g=
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/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
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/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
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.5/go.mod h1:HjQaNPq5SdNazGlwiim6/lWBkZD97mPPyDttr7byNA4=
github.com/pion/webrtc/v3 v3.0.0-beta.10 h1:1aBn9jv/oe4v2Uf47HutWIjg2i2ZP/O7HqpgKPqSuhE=
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.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
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/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=
@ -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-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-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-20181017192945-9dcd33a902f4/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-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-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.3.0/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-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-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-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=

View File

@ -2,6 +2,7 @@
package config
import (
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
"net"
"github.com/sherifabdlnaby/configuro"
@ -23,6 +24,7 @@ type Config struct {
Auth auth.Options
Forwarding forwarding.Options
Monitoring monitoring.Options
OME ovenmediaengine.Options
Srt srt.Options
Telnet telnet.Options
Transcoder transcoder.Options
@ -40,6 +42,7 @@ func New() *Config {
Credentials: make(map[string]string),
},
LDAP: ldap.Options{
Aliases: make(map[string]map[string]string),
URI: "ldap://127.0.0.1:389",
UserDn: "cn=users,dc=example,dc=com",
},
@ -49,6 +52,11 @@ func New() *Config {
Enabled: true,
ListenAddress: ":2112",
},
OME: ovenmediaengine.Options{
Enabled: true,
URL: "ovenmediaengine:1915",
App: "play",
},
Srt: srt.Options{
Enabled: true,
ListenAddress: ":9710",
@ -75,10 +83,15 @@ func New() *Config {
MapDomainToStream: make(map[string]string),
PlayerPoster: "/static/img/no_stream.svg",
ViewersCounterRefreshPeriod: 20000,
LegalMentionsEntity: "l'association Crans",
LegalMentionsAddress: "61 Avenue du Président Wilson, 94235 Cachan Cedex, France",
LegalMentionsFullAddress: []string{"Association Cr@ns - ENS Paris-Saclay",
"Notification de Contenus Illicites", "4, avenue des Sciences", "91190 Gif-sur-Yvette", "France"},
LegalMentionsEmail: "bureau[at]crans.org",
},
WebRTC: webrtc.Options{
Enabled: true,
MaxPortUDP: 10005,
Enabled: false,
MaxPortUDP: 11000,
MinPortUDP: 10000,
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 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

View File

@ -5,8 +5,10 @@
package main
import (
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
"log"
"github.com/pkg/profile"
"gitlab.crans.org/nounous/ghostream/auth"
"gitlab.crans.org/nounous/ghostream/internal/config"
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
@ -20,6 +22,9 @@ import (
)
func main() {
// TODO Don't always profile if not needed
defer profile.Start().Stop()
// Configure logger
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
@ -45,9 +50,10 @@ func main() {
go transcoder.Init(streams, &cfg.Transcoder)
go forwarding.Serve(streams, cfg.Forwarding)
go monitoring.Serve(&cfg.Monitoring)
go ovenmediaengine.Serve(streams, &cfg.OME)
go srt.Serve(streams, authBackend, &cfg.Srt)
go telnet.Serve(streams, &cfg.Telnet)
go web.Serve(streams, &cfg.Web)
go web.Serve(streams, &cfg.Web, &cfg.OME)
go webrtc.Serve(streams, &cfg.WebRTC)
// Wait for routines

View File

@ -40,6 +40,7 @@ func Serve(streams *messaging.Streams, cfg Options) {
stream, err := streams.Get(name)
if err != nil {
log.Printf("Failed to get stream '%s'", name)
return
}
// Get specific quality
@ -74,8 +75,8 @@ func forward(streamName string, q *messaging.Quality, fwdCfg []string) {
formattedURL = strings.ReplaceAll(formattedURL, "%S", fmt.Sprintf("%02d", now.Second()))
formattedURL = strings.ReplaceAll(formattedURL, "%name", streamName)
params = append(params, "-f", "flv", "-preset", "ultrafast", "-tune", "zerolatency",
"-c", "copy", formattedURL)
params = append(params, "-f", "flv",
"-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-ar", "44100", formattedURL)
}
ffmpeg := exec.Command("ffmpeg", params...)

View 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
}
}
}

View File

@ -1,9 +1,6 @@
// Package srt serves a SRT server
package srt
// #include <srt/srt.h>
import "C"
import (
"log"
"net"
@ -62,7 +59,7 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) {
for {
// Wait for new connection
s, err := sck.Accept()
s, _, err := sck.Accept()
if err != nil {
// Something wrong happened
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
// 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 {
log.Print("Failed to get socket streamid")
continue
@ -82,20 +79,22 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) {
if len(split) > 1 {
// password was provided so it is a streamer
name, password := split[0], split[1]
name, password := strings.ToLower(split[0]), split[1]
if authBackend != nil {
// check password
if ok, err := authBackend.Login(name, password); !ok || err != nil {
ok, username, err := authBackend.Login(name, password)
if !ok || err != nil {
log.Printf("Failed to authenticate for stream %s", name)
s.Close()
continue
}
name = username
}
go handleStreamer(s, streams, name)
} else {
// password was not provided so it is a viewer
name := split[0]
name := strings.ToLower(split[0])
// Send stream
go handleViewer(s, streams, name)

View File

@ -3,7 +3,9 @@ package webrtc
import (
"bufio"
"fmt"
"log"
"math/rand"
"net"
"os/exec"
@ -17,20 +19,23 @@ func ingest(name string, q *messaging.Quality) {
videoInput := make(chan []byte, 1024)
q.Register(videoInput)
// Open a UDP Listener for RTP Packets on port 5004
audioListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5004})
// FIXME Mux into RTP without having multiple UDP listeners
firstPort := int(rand.Int31n(63535)) + 2000
// Open UDP listeners for RTP Packets
audioListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: firstPort})
if err != nil {
log.Printf("Faited to open UDP listener %s", err)
return
}
videoListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5005})
videoListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: firstPort + 1})
if err != nil {
log.Printf("Faited to open UDP listener %s", err)
return
}
// Start ffmpag to convert videoInput to video and audio UDP
ffmpeg, err := startFFmpeg(videoInput)
ffmpeg, err := startFFmpeg(videoInput, firstPort)
if err != nil {
log.Printf("Error while starting ffmpeg: %s", err)
return
@ -115,14 +120,14 @@ func ingest(name string, q *messaging.Quality) {
q.Unregister(videoInput)
}
func startFFmpeg(in <-chan []byte) (ffmpeg *exec.Cmd, err error) {
func startFFmpeg(in <-chan []byte, listeningPort int) (ffmpeg *exec.Cmd, err error) {
ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0",
// Audio
"-vn", "-c:a", "libopus", "-b:a", "160k",
"-f", "rtp", "rtp://127.0.0.1:5004",
"-vn", "-c:a", "libopus", "-b:a", "96k",
"-f", "rtp", fmt.Sprintf("rtp://127.0.0.1:%d", listeningPort),
// Source
"-an", "-c:v", "copy", "-b:v", "3000k", "-maxrate", "5000k", "-bufsize", "5000k",
"-f", "rtp", "rtp://127.0.0.1:5005"}
"-an", "-c:v", "copy",
"-f", "rtp", fmt.Sprintf("rtp://127.0.0.1:%d", listeningPort+1)}
ffmpeg = exec.Command("ffmpeg", ffmpegArgs...)
// Handle errors output

View File

@ -10,15 +10,21 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/markbates/pkger"
"gitlab.crans.org/nounous/ghostream/internal/monitoring"
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
"gitlab.crans.org/nounous/ghostream/stream/webrtc"
)
var (
// Precompile regex
validPath = regexp.MustCompile("^/[a-z0-9@_-]*$")
validPath = regexp.MustCompile("^/[a-zA-Z0-9@_-]*$")
counterMutex = new(sync.Mutex)
connectedClients = make(map[string]map[string]int64)
)
// Handle site index and viewer pages
@ -36,7 +42,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
}
// Get stream ID from URL, or from domain name
path := r.URL.Path[1:]
path := strings.ToLower(r.URL.Path[1:])
host := r.Host
if strings.Contains(host, ":") {
realHost, _, err := net.SplitHostPort(r.Host)
@ -52,7 +58,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
if path == "about" {
path = ""
} else {
path = streamID
path = strings.ToLower(streamID)
}
}
@ -61,7 +67,8 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
Cfg *Options
Path string
WidgetURL string
}{Path: path, Cfg: cfg, WidgetURL: ""}
OMECfg *ovenmediaengine.Options
}{Path: path, Cfg: cfg, WidgetURL: "", OMECfg: omeCfg}
// Load widget is user does not disable it with ?nowidget
if _, ok := r.URL.Query()["nowidget"]; !ok {
@ -88,14 +95,44 @@ func staticHandler() http.Handler {
}
func statisticsHandler(w http.ResponseWriter, r *http.Request) {
// Retrieve stream name from URL
name := strings.SplitN(strings.Replace(r.URL.Path[7:], "/", "", -1), "@", 2)[0]
name = strings.ToLower(name)
userCount := 0
// Clients have a unique generated identifier per session, that expires in 40 seconds.
// Each time the client connects to this page, the identifier is renewed.
// Yeah, that's not a good way to have stats, but it works...
if connectedClients[name] == nil {
counterMutex.Lock()
connectedClients[name] = make(map[string]int64)
counterMutex.Unlock()
}
currentTime := time.Now().Unix()
if _, ok := r.URL.Query()["uid"]; ok {
uid := r.URL.Query()["uid"][0]
counterMutex.Lock()
connectedClients[name][uid] = currentTime
counterMutex.Unlock()
}
toDelete := make([]string, 0)
counterMutex.Lock()
for uid, oldTime := range connectedClients[name] {
if currentTime-oldTime > 40 {
toDelete = append(toDelete, uid)
}
}
for _, uid := range toDelete {
delete(connectedClients[name], uid)
}
counterMutex.Unlock()
// Get requested stream
stream, err := streams.Get(name)
if err == nil {
userCount = stream.ClientCount()
userCount += webrtc.GetNumberConnectedSessions(name)
userCount += len(connectedClients[name])
}
// Display connected users statistics

View File

@ -9,6 +9,7 @@ export class ViewerCounter {
constructor(element, streamName) {
this.element = element;
this.url = "/_stats/" + streamName;
this.uid = Math.floor(1e19 * Math.random()).toString(16);
}
/**
@ -21,7 +22,7 @@ export class ViewerCounter {
}
refreshViewersCounter() {
fetch(this.url)
fetch(this.url + "?uid=" + this.uid)
.then(response => response.json())
.then((data) => this.element.innerText = data.ConnectedViewers)
.catch(console.log);

116
web/static/js/ovenplayer.js Normal file
View 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;
}
});
}

File diff suppressed because one or more lines are too long

View 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

View 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 */

View 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

View 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

View 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

View 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

View 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 */

View File

@ -9,7 +9,11 @@
</p>
<h2>Comment je diffuse ?</h2>
<p>Pour diffuser un contenu vous devez être adhérent Crans.</p>
<p>
Pour diffuser un contenu vous devez avoir des identifiants valides.
Si le service est hébergé par une association, il est probable que
vous deviez être membre de cette association.
</p>
<h3>Avec Open Broadcaster Software</h3>
<p>
@ -21,11 +25,7 @@
<ul>
<li>
<b>Serveur :</b>
<code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}</code>,
</li>
<li>
<b>Clé de stream :</b>
<code>IDENTIFIANT:MOT_DE_PASSE</code>
<code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT:MOT_DE_PASSE</code>,
avec <code>IDENTIFIANT</code> et <code>MOT_DE_PASSE</code>
vos identifiants.
</li>
@ -46,29 +46,104 @@
<p>
<code>
{{/* FIXME replace with good SRT params */}}
ffmpeg -re -i mavideo.webm -vcodec libx264 -vprofile baseline
ffmpeg -re -i mavideo.webm -vcodec libx264
-preset:v veryfast -vprofile baseline -tune zerolatency
-acodec aac -strict -2 -f flv
srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=IDENTIFIANT:MOT_DE_PASSE
</code>
</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 &le; 20.04 ou
Debian &le; 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>
<p>
Le service de diffusion vidéo du Crans est un service d'hébergement
au sens de l'article 6, I, 2e de la loi 2004-575 du 21 juin 2004.
Conformément aux dispositions de l'article 6, II du même,
l'association Crans conserve les données de nature à permettre
conserve les données de nature à permettre
l'identification des auteurs du contenu diffusé.
Ce service est hébergé par l'association Crans, au
61 Avenue du Président Wilson, 94235 Cachan Cedex, France.
Ce service est hébergé par {{.Cfg.LegalMentionsEntity}}, au
{{.Cfg.LegalMentionsAddress}}.
</p>
<p>
<b>En cas de réclamation sur le contenu diffusé</b>,
la loi vous autorise à contacter directement l'hébergeur à
l'adresse suivante :
<pre>Association Cr@ns - ENS Paris-Saclay<br/>Notification de Contenus Illicites<br/>4, avenue des Sciences<br/>91190 Gif-sur-Yvette<br/>France</pre>
<pre>{{range $i, $element := .Cfg.LegalMentionsFullAddress}}{{$element}}<br/>{{end}}</pre>
Vous pouvez également envoyer directement vos réclamations par
courrier électronique à l'adresse <code>bureau[at]crans.org</code>.
courrier électronique à l'adresse <code>{{.Cfg.LegalMentionsEmail}}</code>.
</p>
</div>
{{end}}

View File

@ -6,14 +6,14 @@
<!-- Links and settings under video -->
<div class="controls">
<span class="control-quality">
<!-- <span class="control-quality">
<select id="quality">
<option value="source">Source</option>
<option value="720p">720p</option>
<option value="480p">480p</option>
<option value="240p">240p</option>
</select>
</span>
</span> -->
<code class="control-srt-link">srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid={{.Path}}</code>
<span class="control-viewers" id="connected-people">0</span>
<svg class="control-indicator" id="connectionIndicator" fill="#dc3545" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
@ -34,8 +34,18 @@
{{end}}
</div>
{{if .OMECfg.Enabled}}
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dashjs/2.9.3/dash.all.min.js"></script>
<script src="/static/ovenplayer/ovenplayer.js"></script>
<script src="/static/js/ovenplayer.js"></script>
{{end}}
<script type="module">
{{if .OMECfg.Enabled}}
import { initViewerPage } from "/static/js/ovenplayer.js";
{{else}}
import { initViewerPage } from "/static/js/viewer.js";
{{end}}
// Some variables that need to be fixed by web page
const viewersCounterRefreshPeriod = Number("{{.Cfg.ViewersCounterRefreshPeriod}}");
@ -45,6 +55,10 @@
"{{$value}}",
{{end}}
]
{{if .OMECfg.Enabled}}
initViewerPage(stream, {{.OMECfg.App}}, viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}})
{{else}}
initViewerPage(stream, stunServers, viewersCounterRefreshPeriod)
{{end}}
</script>
{{end}}

View File

@ -2,6 +2,7 @@
package web
import (
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
"html/template"
"io/ioutil"
"log"
@ -27,11 +28,17 @@ type Options struct {
STUNServers []string
ViewersCounterRefreshPeriod int
WidgetURL string
LegalMentionsEntity string
LegalMentionsAddress string
LegalMentionsFullAddress []string
LegalMentionsEmail string
}
var (
cfg *Options
omeCfg *ovenmediaengine.Options
// Preload templates
templates *template.Template
@ -70,9 +77,10 @@ func loadTemplates() error {
}
// Serve HTTP server
func Serve(s *messaging.Streams, c *Options) {
func Serve(s *messaging.Streams, c *Options, ome *ovenmediaengine.Options) {
streams = s
cfg = c
omeCfg = ome
if !cfg.Enabled {
// Web server is not enabled, ignore

View File

@ -36,7 +36,8 @@ func websocketHandler(w http.ResponseWriter, r *http.Request) {
err = conn.ReadJSON(c)
if err != nil {
log.Printf("Failed to receive client description: %s", err)
continue
_ = conn.Close()
return
}
// Get requested stream