mirror of
https://gitlab.crans.org/nounous/ghostream.git
synced 2025-07-01 16:51:16 +02:00
Compare commits
36 Commits
h264-reade
...
dev
Author | SHA1 | Date | |
---|---|---|---|
3493ba5e2f | |||
f7cf187bac
|
|||
dc594d091c
|
|||
a429216735
|
|||
e6fd4f6352
|
|||
34652f8f3e
|
|||
79f52ed880
|
|||
ee16bf9e21
|
|||
e47aefd6df
|
|||
7e0ee7aba5
|
|||
8d2adad509 | |||
0035c63c22 | |||
849196b4cb | |||
205c4b526c | |||
1d117ea480 | |||
45cb61e436 | |||
7e4adb475a | |||
d1c4f81f4e | |||
b2104a0cb7 | |||
6ca354f44f | |||
a20c6202fd | |||
b52f377b6b | |||
3d8ba0623d | |||
cfcde6f530 | |||
28ef6a5526 | |||
5ad8a69c4c | |||
d334556d2b | |||
9625cba5e1 | |||
e74acf04f7 | |||
2085d13c0d | |||
85a5606291 | |||
33f86a0742 | |||
11d89c6950 | |||
c9a2d5b359 | |||
ee927c5b8f | |||
955364a5fc |
@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
This project was developped at [Cr@ns](https://crans.org/) to stream events.
|
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:
|
Features:
|
||||||
|
|
||||||
- WebRTC playback with a lightweight web interface.
|
- WebRTC playback with a lightweight web interface.
|
||||||
|
@ -20,7 +20,7 @@ type Options struct {
|
|||||||
|
|
||||||
// Backend to log user in
|
// Backend to log user in
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
Login(string, string) (bool, error)
|
Login(string, string) (bool, string, error)
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,15 +23,15 @@ type Basic struct {
|
|||||||
|
|
||||||
// Login hashs password and compare
|
// Login hashs password and compare
|
||||||
// Returns (true, nil) if success
|
// 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]
|
hash, ok := a.Cfg.Credentials[username]
|
||||||
if !ok {
|
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))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
|
||||||
// Login succeeded if no error
|
// Login succeeded if no error
|
||||||
return err == nil, err
|
return err == nil, username, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close has no connection to close
|
// Close has no connection to close
|
||||||
|
@ -10,19 +10,19 @@ func TestBasicLogin(t *testing.T) {
|
|||||||
|
|
||||||
// Test good credentials
|
// Test good credentials
|
||||||
backend, _ := New(&Options{Credentials: basicCredentials})
|
backend, _ := New(&Options{Credentials: basicCredentials})
|
||||||
ok, err := backend.Login("demo", "demo")
|
ok, _, err := backend.Login("demo", "demo")
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("Error while logging with the basic authentication:", err)
|
t.Error("Error while logging with the basic authentication:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test bad username
|
// Test bad username
|
||||||
ok, err = backend.Login("baduser", "demo")
|
ok, _, err = backend.Login("baduser", "demo")
|
||||||
if ok {
|
if ok {
|
||||||
t.Error("Authentification failed to fail:", err)
|
t.Error("Authentification failed to fail:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test bad password
|
// Test bad password
|
||||||
ok, err = backend.Login("demo", "badpass")
|
ok, _, err = backend.Login("demo", "badpass")
|
||||||
if ok {
|
if ok {
|
||||||
t.Error("Authentification failed to fail:", err)
|
t.Error("Authentification failed to fail:", err)
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,15 @@ package ldap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options holds package configuration
|
// Options holds package configuration
|
||||||
type Options struct {
|
type Options struct {
|
||||||
URI string
|
Aliases map[string]map[string]string
|
||||||
UserDn string
|
URI string
|
||||||
|
UserDn string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAP authentification backend
|
// LDAP authentification backend
|
||||||
@ -19,13 +22,37 @@ 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, string, error) {
|
||||||
// Try to bind as user
|
aliasSplit := strings.SplitN(username, "__", 2)
|
||||||
bindDn := "cn=" + username + "," + a.Cfg.UserDn
|
potentialUsernames := []string{username}
|
||||||
err := a.Conn.Bind(bindDn, password)
|
|
||||||
|
|
||||||
// Login succeeded if no error
|
if len(aliasSplit) == 2 {
|
||||||
return err == nil, err
|
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
|
||||||
|
log.Printf("[LDAP] Logging to %s...", bindDn)
|
||||||
|
err = a.Conn.Bind(bindDn, password)
|
||||||
|
if err == nil {
|
||||||
|
// Login succeeded if no error
|
||||||
|
return true, aliasSplit[0], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[LDAP] Logging failed: %s", err)
|
||||||
|
// Unable to log in
|
||||||
|
return err == nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close LDAP connection
|
// Close LDAP connection
|
||||||
|
118
docs/Server-docker.xml
Normal file
118
docs/Server-docker.xml
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<Server version="7">
|
||||||
|
<Name>OvenMediaEngine</Name>
|
||||||
|
<Type>origin</Type>
|
||||||
|
<IP>*</IP>
|
||||||
|
|
||||||
|
<Bind>
|
||||||
|
<Providers>
|
||||||
|
<RTMP>
|
||||||
|
<Port>1915</Port>
|
||||||
|
</RTMP>
|
||||||
|
</Providers>
|
||||||
|
<Publishers>
|
||||||
|
<WebRTC>
|
||||||
|
<Signalling>
|
||||||
|
<Port>3333</Port>
|
||||||
|
</Signalling>
|
||||||
|
<IceCandidates>
|
||||||
|
<IceCandidate>*:10006-10010/udp</IceCandidate>
|
||||||
|
</IceCandidates>
|
||||||
|
</WebRTC>
|
||||||
|
<HLS>
|
||||||
|
<Port>80</Port>
|
||||||
|
</HLS>
|
||||||
|
<DASH>
|
||||||
|
<Port>80</Port>
|
||||||
|
</DASH>
|
||||||
|
</Publishers>
|
||||||
|
</Bind>
|
||||||
|
|
||||||
|
<VirtualHosts>
|
||||||
|
<VirtualHost>
|
||||||
|
<Name>default</Name>
|
||||||
|
<Domain>
|
||||||
|
<Names>
|
||||||
|
<Name>*</Name>
|
||||||
|
</Names>
|
||||||
|
</Domain>
|
||||||
|
<Applications>
|
||||||
|
<Application>
|
||||||
|
<Name>play</Name>
|
||||||
|
<Type>live</Type>
|
||||||
|
<Encodes>
|
||||||
|
<Encode>
|
||||||
|
<Name>opus_only</Name>
|
||||||
|
<Audio>
|
||||||
|
<Codec>opus</Codec>
|
||||||
|
<Bitrate>128000</Bitrate>
|
||||||
|
<Samplerate>48000</Samplerate>
|
||||||
|
<Channel>2</Channel>
|
||||||
|
</Audio>
|
||||||
|
<Video>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Video>
|
||||||
|
</Encode>
|
||||||
|
<Encode>
|
||||||
|
<Name>bypass</Name>
|
||||||
|
<Audio>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Audio>
|
||||||
|
<Video>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Video>
|
||||||
|
</Encode>
|
||||||
|
|
||||||
|
</Encodes>
|
||||||
|
<Streams>
|
||||||
|
<Stream>
|
||||||
|
<Name>${OriginStreamName}</Name>
|
||||||
|
<Profiles>
|
||||||
|
<Profile>opus_only</Profile>
|
||||||
|
</Profiles>
|
||||||
|
</Stream>
|
||||||
|
<Stream>
|
||||||
|
<Name>${OriginStreamName}_bypass</Name>
|
||||||
|
<Profiles>
|
||||||
|
<Profile>bypass</Profile>
|
||||||
|
</Profiles>
|
||||||
|
</Stream>
|
||||||
|
|
||||||
|
</Streams>
|
||||||
|
<Providers>
|
||||||
|
<RTMP>
|
||||||
|
<BlockDuplicateStreamName>true</BlockDuplicateStreamName>
|
||||||
|
</RTMP>
|
||||||
|
</Providers>
|
||||||
|
<Publishers>
|
||||||
|
<ThreadCount>2</ThreadCount>
|
||||||
|
<WebRTC>
|
||||||
|
<Timeout>30000</Timeout>
|
||||||
|
</WebRTC>
|
||||||
|
<HLS>
|
||||||
|
<SegmentDuration>2</SegmentDuration>
|
||||||
|
<SegmentCount>2</SegmentCount>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</HLS>
|
||||||
|
<DASH>
|
||||||
|
<SegmentDuration>2</SegmentDuration>
|
||||||
|
<SegmentCount>2</SegmentCount>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</DASH>
|
||||||
|
<LLDASH>
|
||||||
|
<SegmentDuration>2</SegmentDuration>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</LLDASH>
|
||||||
|
</Publishers>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
</VirtualHost>
|
||||||
|
</VirtualHosts>
|
||||||
|
</Server>
|
@ -26,11 +26,10 @@ services:
|
|||||||
- "--certificatesResolvers.mytlschallenge.acme.httpChallenge.entryPoint=web"
|
- "--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-11000:10000-11000/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"
|
||||||
|
@ -34,6 +34,13 @@ auth:
|
|||||||
#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.
|
||||||
|
# # You will have to use the streamid example__demo:password
|
||||||
|
# aliases:
|
||||||
|
# example:
|
||||||
|
# demo: ignored
|
||||||
|
#
|
||||||
|
|
||||||
## Stream forwarding ##
|
## Stream forwarding ##
|
||||||
# Forward an incoming stream to other servers
|
# Forward an incoming stream to other servers
|
||||||
@ -61,6 +68,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,11 +180,23 @@ 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.
|
||||||
|
@ -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,8 +42,9 @@ func New() *Config {
|
|||||||
Credentials: make(map[string]string),
|
Credentials: make(map[string]string),
|
||||||
},
|
},
|
||||||
LDAP: ldap.Options{
|
LDAP: ldap.Options{
|
||||||
URI: "ldap://127.0.0.1:389",
|
Aliases: make(map[string]map[string]string),
|
||||||
UserDn: "cn=users,dc=example,dc=com",
|
URI: "ldap://127.0.0.1:389",
|
||||||
|
UserDn: "cn=users,dc=example,dc=com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Forwarding: make(map[string][]string),
|
Forwarding: make(map[string][]string),
|
||||||
@ -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,9 +83,14 @@ 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: 11000,
|
MaxPortUDP: 11000,
|
||||||
MinPortUDP: 10000,
|
MinPortUDP: 10000,
|
||||||
STUNServers: []string{"stun:stun.l.google.com:19302"},
|
STUNServers: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
4
main.go
4
main.go
@ -5,6 +5,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
@ -49,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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -79,20 +79,22 @@ func Serve(streams *messaging.Streams, authBackend auth.Backend, cfg *Options) {
|
|||||||
|
|
||||||
if len(split) > 1 {
|
if len(split) > 1 {
|
||||||
// password was provided so it is a streamer
|
// 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 {
|
if authBackend != nil {
|
||||||
// check password
|
// 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)
|
log.Printf("Failed to authenticate for stream %s", name)
|
||||||
s.Close()
|
s.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
name = username
|
||||||
}
|
}
|
||||||
|
|
||||||
go handleStreamer(s, streams, name)
|
go handleStreamer(s, streams, name)
|
||||||
} else {
|
} else {
|
||||||
// password was not provided so it is a viewer
|
// password was not provided so it is a viewer
|
||||||
name := split[0]
|
name := strings.ToLower(split[0])
|
||||||
|
|
||||||
// Send stream
|
// Send stream
|
||||||
go handleViewer(s, streams, name)
|
go handleViewer(s, streams, name)
|
||||||
|
@ -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-zA-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
|
||||||
@ -36,7 +42,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get stream ID from URL, or from domain name
|
// Get stream ID from URL, or from domain name
|
||||||
path := r.URL.Path[1:]
|
path := strings.ToLower(r.URL.Path[1:])
|
||||||
host := r.Host
|
host := r.Host
|
||||||
if strings.Contains(host, ":") {
|
if strings.Contains(host, ":") {
|
||||||
realHost, _, err := net.SplitHostPort(r.Host)
|
realHost, _, err := net.SplitHostPort(r.Host)
|
||||||
@ -52,7 +58,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if path == "about" {
|
if path == "about" {
|
||||||
path = ""
|
path = ""
|
||||||
} else {
|
} else {
|
||||||
path = streamID
|
path = strings.ToLower(streamID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,44 @@ 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]
|
||||||
|
name = strings.ToLower(name)
|
||||||
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
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,7 +25,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<b>Serveur :</b>
|
<b>Serveur :</b>
|
||||||
<code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?IDENTIFIANT:MOT_DE_PASS</code>,
|
<code>srt://{{.Cfg.Hostname}}:{{.Cfg.SRTServerPort}}?streamid=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>
|
||||||
@ -42,7 +46,8 @@
|
|||||||
<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>
|
||||||
@ -95,10 +100,9 @@
|
|||||||
Bien que VLC supporte officiellement le protocole SRT,
|
Bien que VLC supporte officiellement le protocole SRT,
|
||||||
toutes les options ne sont pas encore implémentées,
|
toutes les options ne sont pas encore implémentées,
|
||||||
notamment l'option pour choisir son stream.
|
notamment l'option pour choisir son stream.
|
||||||
<a href="https://patches.videolan.org/patch/30299/">Un patch</a>
|
Cette option n'est supportée que dans la version de développement
|
||||||
a été soumis et est en attente d'acceptation.
|
depuis très récemment, grâce à un patch de l'un des développeurs
|
||||||
Une fois le patch accepté, il sera appliqué dans les versions
|
de Ghostream. Sous Arch Linux, il suffit de récupérer
|
||||||
de développement de VLC. Sous Arch Linux, il suffit de récupérer
|
|
||||||
le paquet <code>vlc-git</code> de l'AUR. Avec un VLC à jour,
|
le paquet <code>vlc-git</code> de l'AUR. Avec un VLC à jour,
|
||||||
il suffit d'exécuter :
|
il suffit d'exécuter :
|
||||||
</p>
|
</p>
|
||||||
@ -128,18 +132,18 @@
|
|||||||
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="source">Source</option>
|
<option value="source">Source</option>
|
||||||
<option value="720p">720p</option>
|
<option value="720p">720p</option>
|
||||||
<option value="480p">480p</option>
|
<option value="480p">480p</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">
|
||||||
import { initViewerPage } from "/static/js/viewer.js";
|
{{if .OMECfg.Enabled}}
|
||||||
|
import { initViewerPage } from "/static/js/ovenplayer.js";
|
||||||
|
{{else}}
|
||||||
|
import { initViewerPage } from "/static/js/viewer.js";
|
||||||
|
{{end}}
|
||||||
|
|
||||||
// Some variables that need to be fixed by web page
|
// 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}}
|
||||||
]
|
]
|
||||||
initViewerPage(stream, stunServers, viewersCounterRefreshPeriod)
|
{{if .OMECfg.Enabled}}
|
||||||
|
initViewerPage(stream, {{.OMECfg.App}}, viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}})
|
||||||
|
{{else}}
|
||||||
|
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
|
||||||
|
Reference in New Issue
Block a user