mirror of
https://gitlab.crans.org/nounous/ghostream.git
synced 2025-07-28 15:01:33 +02:00
Compare commits
34 Commits
ee927c5b8f
...
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 |
@ -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,10 +3,13 @@ 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 {
|
||||||
|
Aliases map[string]map[string]string
|
||||||
URI string
|
URI string
|
||||||
UserDn string
|
UserDn string
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
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
|
// Try to bind as user
|
||||||
bindDn := "cn=" + username + "," + a.Cfg.UserDn
|
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
|
// 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
|
// Close LDAP connection
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
<HLS>
|
<HLS>
|
||||||
<Port>80</Port>
|
<Port>80</Port>
|
||||||
</HLS>
|
</HLS>
|
||||||
|
<DASH>
|
||||||
|
<Port>80</Port>
|
||||||
|
</DASH>
|
||||||
</Publishers>
|
</Publishers>
|
||||||
</Bind>
|
</Bind>
|
||||||
|
|
||||||
@ -36,7 +39,7 @@
|
|||||||
</Domain>
|
</Domain>
|
||||||
<Applications>
|
<Applications>
|
||||||
<Application>
|
<Application>
|
||||||
<Name>app</Name>
|
<Name>play</Name>
|
||||||
<Type>live</Type>
|
<Type>live</Type>
|
||||||
<Encodes>
|
<Encodes>
|
||||||
<Encode>
|
<Encode>
|
||||||
@ -52,14 +55,15 @@
|
|||||||
</Video>
|
</Video>
|
||||||
</Encode>
|
</Encode>
|
||||||
<Encode>
|
<Encode>
|
||||||
<Name>BYPASS</Name>
|
<Name>bypass</Name>
|
||||||
<Video>
|
|
||||||
<Bypass>true</Bypass>
|
|
||||||
</Video>
|
|
||||||
<Audio>
|
<Audio>
|
||||||
<Bypass>true</Bypass>
|
<Bypass>true</Bypass>
|
||||||
</Audio>
|
</Audio>
|
||||||
|
<Video>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Video>
|
||||||
</Encode>
|
</Encode>
|
||||||
|
|
||||||
</Encodes>
|
</Encodes>
|
||||||
<Streams>
|
<Streams>
|
||||||
<Stream>
|
<Stream>
|
||||||
@ -71,9 +75,10 @@
|
|||||||
<Stream>
|
<Stream>
|
||||||
<Name>${OriginStreamName}_bypass</Name>
|
<Name>${OriginStreamName}_bypass</Name>
|
||||||
<Profiles>
|
<Profiles>
|
||||||
<Profile>BYPASS</Profile>
|
<Profile>bypass</Profile>
|
||||||
</Profiles>
|
</Profiles>
|
||||||
</Stream>
|
</Stream>
|
||||||
|
|
||||||
</Streams>
|
</Streams>
|
||||||
<Providers>
|
<Providers>
|
||||||
<RTMP>
|
<RTMP>
|
||||||
@ -86,12 +91,25 @@
|
|||||||
<Timeout>30000</Timeout>
|
<Timeout>30000</Timeout>
|
||||||
</WebRTC>
|
</WebRTC>
|
||||||
<HLS>
|
<HLS>
|
||||||
<SegmentDuration>5</SegmentDuration>
|
<SegmentDuration>2</SegmentDuration>
|
||||||
<SegmentCount>2</SegmentCount>
|
<SegmentCount>2</SegmentCount>
|
||||||
<CrossDomain>
|
<CrossDomain>
|
||||||
<Url>*</Url>
|
<Url>*</Url>
|
||||||
</CrossDomain>
|
</CrossDomain>
|
||||||
</HLS>
|
</HLS>
|
||||||
|
<DASH>
|
||||||
|
<SegmentDuration>2</SegmentDuration>
|
||||||
|
<SegmentCount>2</SegmentCount>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</DASH>
|
||||||
|
<LLDASH>
|
||||||
|
<SegmentDuration>2</SegmentDuration>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</LLDASH>
|
||||||
</Publishers>
|
</Publishers>
|
||||||
</Application>
|
</Application>
|
||||||
</Applications>
|
</Applications>
|
||||||
|
@ -51,7 +51,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
|
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
|
|
||||||
- "traefik.http.routers.ovenmediaengine.rule=Host(`stream.example.com`) && PathPrefix(`/app/`)"
|
- "traefik.http.routers.ovenmediaengine.rule=Host(`stream.example.com`) && PathPrefix(`/play/`)"
|
||||||
- "traefik.http.routers.ovenmediaengine.priority=101"
|
- "traefik.http.routers.ovenmediaengine.priority=101"
|
||||||
- "traefik.http.routers.ovenmediaengine.entrypoints=websecure"
|
- "traefik.http.routers.ovenmediaengine.entrypoints=websecure"
|
||||||
- "traefik.http.routers.ovenmediaengine.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.ovenmediaengine.tls.certresolver=mytlschallenge"
|
||||||
@ -59,7 +59,7 @@ services:
|
|||||||
- "traefik.http.routers.ovenmediaengine.service=ovenmediaengine"
|
- "traefik.http.routers.ovenmediaengine.service=ovenmediaengine"
|
||||||
- "traefik.http.routers.ovenmediaengine.middlewares=sslheader"
|
- "traefik.http.routers.ovenmediaengine.middlewares=sslheader"
|
||||||
|
|
||||||
- "traefik.http.routers.ovenmediaengine-hls.rule=Host(`stream.example.com`) && Path(`/app/{app_name:.*}/{filename:.*}.{ext:(m3u8|mpd|ts)}`)"
|
- "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.priority=102"
|
||||||
- "traefik.http.routers.ovenmediaengine-hls.entrypoints=websecure"
|
- "traefik.http.routers.ovenmediaengine-hls.entrypoints=websecure"
|
||||||
- "traefik.http.routers.ovenmediaengine-hls.tls.certresolver=mytlschallenge"
|
- "traefik.http.routers.ovenmediaengine-hls.tls.certresolver=mytlschallenge"
|
||||||
|
@ -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,6 +180,18 @@ 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.
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gitlab.crans.org/nounous/ghostream/stream/ovenmediaengine"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sherifabdlnaby/configuro"
|
"github.com/sherifabdlnaby/configuro"
|
||||||
@ -23,6 +24,7 @@ type Config struct {
|
|||||||
Auth auth.Options
|
Auth auth.Options
|
||||||
Forwarding forwarding.Options
|
Forwarding forwarding.Options
|
||||||
Monitoring monitoring.Options
|
Monitoring monitoring.Options
|
||||||
|
OME ovenmediaengine.Options
|
||||||
Srt srt.Options
|
Srt srt.Options
|
||||||
Telnet telnet.Options
|
Telnet telnet.Options
|
||||||
Transcoder transcoder.Options
|
Transcoder transcoder.Options
|
||||||
@ -40,6 +42,7 @@ func New() *Config {
|
|||||||
Credentials: make(map[string]string),
|
Credentials: make(map[string]string),
|
||||||
},
|
},
|
||||||
LDAP: ldap.Options{
|
LDAP: ldap.Options{
|
||||||
|
Aliases: make(map[string]map[string]string),
|
||||||
URI: "ldap://127.0.0.1:389",
|
URI: "ldap://127.0.0.1:389",
|
||||||
UserDn: "cn=users,dc=example,dc=com",
|
UserDn: "cn=users,dc=example,dc=com",
|
||||||
},
|
},
|
||||||
@ -49,6 +52,11 @@ func New() *Config {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
ListenAddress: ":2112",
|
ListenAddress: ":2112",
|
||||||
},
|
},
|
||||||
|
OME: ovenmediaengine.Options{
|
||||||
|
Enabled: true,
|
||||||
|
URL: "ovenmediaengine:1915",
|
||||||
|
App: "play",
|
||||||
|
},
|
||||||
Srt: srt.Options{
|
Srt: srt.Options{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
ListenAddress: ":9710",
|
ListenAddress: ":9710",
|
||||||
@ -75,6 +83,11 @@ 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: false,
|
Enabled: false,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
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}}
|
||||||
|
@ -34,8 +34,18 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <script type="module">
|
{{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";
|
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,60 +55,10 @@
|
|||||||
"{{$value}}",
|
"{{$value}}",
|
||||||
{{end}}
|
{{end}}
|
||||||
]
|
]
|
||||||
|
{{if .OMECfg.Enabled}}
|
||||||
|
initViewerPage(stream, {{.OMECfg.App}}, viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}})
|
||||||
|
{{else}}
|
||||||
initViewerPage(stream, stunServers, viewersCounterRefreshPeriod)
|
initViewerPage(stream, stunServers, viewersCounterRefreshPeriod)
|
||||||
</script> -->
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
|
||||||
<script src="/static/ovenplayer/ovenplayer.js"></script>
|
|
||||||
<script>
|
|
||||||
{{if .WidgetURL}}
|
|
||||||
// Toggle chat
|
|
||||||
const chatToggle = document.getElementById("sideWidgetToggle")
|
|
||||||
chatToggle.addEventListener("click", function () {
|
|
||||||
const chatCol = document.getElementById("sideWidget")
|
|
||||||
if (chatCol.style.display === "none") {
|
|
||||||
chatCol.style.display = "block"
|
|
||||||
chatToggle.textContent = "»"
|
|
||||||
} else {
|
|
||||||
chatCol.style.display = "none"
|
|
||||||
chatToggle.textContent = "«"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
// Create player
|
|
||||||
player = OvenPlayer.create("viewer", {
|
|
||||||
title: "Parlons-Maths",
|
|
||||||
image: "{{.Cfg.PlayerPoster}}",
|
|
||||||
autoStart: true,
|
|
||||||
mute: false,
|
|
||||||
expandFullScreenUI: true,
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
"file": "wss://" + window.location.host + "/app/{{.Path}}",
|
|
||||||
"type": "webrtc",
|
|
||||||
"label": " WebRTC - Source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "hls",
|
|
||||||
"file": "https://" + window.location.host + "/app/{{.Path}}_bypass/playlist.m3u8",
|
|
||||||
"label": " HLS"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
player.on("error", function(error){
|
|
||||||
if (error.code === 501) {
|
|
||||||
// Change message
|
|
||||||
const errorMsg = document.getElementsByClassName("op-message-text")[0]
|
|
||||||
errorMsg.textContent = "Le stream semble inactif. Cette page se rafraîchit toutes les 30 secondes."
|
|
||||||
|
|
||||||
// Reload in 5s
|
|
||||||
setTimeout(function () {
|
|
||||||
player.load()
|
|
||||||
}, 30000)
|
|
||||||
} else {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</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