mirror of
https://gitlab.crans.org/nounous/ghostream.git
synced 2024-12-22 15:02:19 +00:00
Initial Golang project
This commit is contained in:
commit
b5a7b9bbcd
14
build/Dockerfile
Normal file
14
build/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Install dependencies and build
|
||||||
|
FROM golang:1.15-alpine AS build_base
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
WORKDIR /code
|
||||||
|
COPY . .
|
||||||
|
RUN go mod download
|
||||||
|
RUN go build -o ./out/ghostream .
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM alpine:3.12
|
||||||
|
RUN apk add ca-certificates
|
||||||
|
COPY --from=build_base /code/out/ghostream /app/ghostream
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/app/ghostream"]
|
12
init/ghostream.service
Normal file
12
init/ghostream.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Simple streaming server
|
||||||
|
After=syslog.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
EnvironmentFile=-/etc/default/ghostream
|
||||||
|
ExecStart=/usr/bin/ghostream
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
51
main.go
Normal file
51
main.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preload templates
|
||||||
|
var templates = template.Must(template.ParseGlob("web/template/*.tmpl"))
|
||||||
|
|
||||||
|
// Handle site index and viewer pages
|
||||||
|
func handlerViewer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Remove traling slash
|
||||||
|
//path := r.URL.Path[1:]
|
||||||
|
|
||||||
|
// Render template
|
||||||
|
err := templates.ExecuteTemplate(w, "base", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth incoming stream
|
||||||
|
func handleStreamAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// FIXME POST request only with "name" and "pass"
|
||||||
|
// if name or pass missing => 400 Malformed request
|
||||||
|
// else login in against LDAP or static users
|
||||||
|
http.Error(w, "Not implemented", 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle static files
|
||||||
|
// We do not use http.FileServer as we do not want directory listing
|
||||||
|
func handleStatic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := "./" + r.URL.Path
|
||||||
|
if f, err := os.Stat(path); err == nil && !f.IsDir() {
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Set up HTTP router and server
|
||||||
|
http.HandleFunc("/", handlerViewer)
|
||||||
|
http.HandleFunc("/rtmp/auth", handleStreamAuth)
|
||||||
|
http.HandleFunc("/static/", handleStatic)
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
98
web/static/style.css
Normal file
98
web/static/style.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
body, html {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #3b4b5b;
|
||||||
|
font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #bbb;
|
||||||
|
text-align: justify;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 87.5%;
|
||||||
|
word-break: break-word;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-video {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-chat {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 1rem 3rem rgba(0,0,0,0.175);
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-chat iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide chat toggler on small screen */
|
||||||
|
#chatToggle {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On large screen, put chat on left */
|
||||||
|
@media(min-width:1000px){
|
||||||
|
.col-chat {
|
||||||
|
width: 33.33333%;
|
||||||
|
flex: 0 0 33.33333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatToggle {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
18
web/template/base.tmpl
Normal file
18
web/template/base.tmpl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{{define "base"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{if .Path}}{{.Path}} - {{end}}SITE_NAME</title>
|
||||||
|
<link rel="stylesheet" href="static/style.css">
|
||||||
|
<link rel="shortcut icon" href="static/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{if .Path}}
|
||||||
|
{{template "viewer" .}}
|
||||||
|
{{else}}
|
||||||
|
{{template "index" .}}
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
75
web/template/index.tmpl
Normal file
75
web/template/index.tmpl
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{{define "index"}}
|
||||||
|
<div style="max-width:720px;margin:0 auto; padding: 1rem">
|
||||||
|
<h1>SITE_NAME</h1>
|
||||||
|
<p>
|
||||||
|
SITE_NAME est un service maintenu par le
|
||||||
|
<a href="https://crans.org/">Crans</a> permettant de diffuser
|
||||||
|
un contenu vidéo. Il a pour but d'être utilisé pour diffuser
|
||||||
|
des séminaires ou évènements.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Comment je diffuse ?</h2>
|
||||||
|
<p>Pour diffuser un contenu vous devez être adhérent Crans.</p>
|
||||||
|
|
||||||
|
<h3>Avec Open Broadcaster Software</h3>
|
||||||
|
<p>
|
||||||
|
<a href="https://obsproject.com/">Open Broadcaster Software</a>
|
||||||
|
est une solution libre et open source de diffusion vidéo.
|
||||||
|
Pour diffuser sur cette plateforme, allez dans les paramètres
|
||||||
|
« <i>Stream (flux)</i> » :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Serveur :</b>
|
||||||
|
<code>rtmps://config.SITE_HOSTNAME:1935/stream</code>,
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Clé de stream :</b>
|
||||||
|
<code>IDENTIFIANT?pass=MOT_DE_PASSE</code>
|
||||||
|
avec <code>IDENTIFIANT</code> et <code>MOT_DE_PASSE</code>
|
||||||
|
vos identifiants.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<img alt="" src="static/obs_stream.png">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Pour éviter les saccades, changez vos paramètres d'encodage dans
|
||||||
|
« <i>Sortie</i> » avec un profil <code>baseline</code> et un
|
||||||
|
règlage fin <code>zerolatency</code>.
|
||||||
|
</p>
|
||||||
|
<img alt="" src="static/obs_sortie.png">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Votre stream sera alors disponible sur
|
||||||
|
<code>https://SITE_HOSTNAME/IDENTIFIANT</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Avec FFmpeg</h3>
|
||||||
|
<p>
|
||||||
|
<code>
|
||||||
|
ffmpeg -re -i mavideo.webm -vcodec libx264 -vprofile baseline
|
||||||
|
-acodec aac -strict -2 -f flv
|
||||||
|
rtmps://SITE_HOSTNAME:1935/stream/IDENTIFIANT?pass=MOT_DE_PASSE
|
||||||
|
</code>
|
||||||
|
</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
|
||||||
|
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.
|
||||||
|
</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>
|
||||||
|
Vous pouvez également envoyer directement vos réclamations par
|
||||||
|
courrier électronique à l'adresse <code>bureau[at]crans.org</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
67
web/template/viewer.tmpl
Normal file
67
web/template/viewer.tmpl
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{{define "viewer"}}
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-video">
|
||||||
|
<!-- Video -->
|
||||||
|
<video id="player"></video>
|
||||||
|
|
||||||
|
<!-- Links under video -->
|
||||||
|
<p>
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-box-arrow-up-right" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 13A1.5 1.5 0 0 0 3 14.5h8a1.5 1.5 0 0 0 1.5-1.5V9a.5.5 0 0 0-1 0v4a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 0 0-1H3A1.5 1.5 0 0 0 1.5 5v8zm7-11a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.5H9a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.354 1.646a.5.5 0 0 1 0 .708l-8 8a.5.5 0 0 1-.708-.708l8-8a.5.5 0 0 1 .708 0z"/>
|
||||||
|
</svg>
|
||||||
|
<code>rtmps://SITE_HOSTNAME:1935/play/{{.path}}</code>
|
||||||
|
<a href="#" id="chatToggle" title="Cacher/Afficher le chat">»</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat -->
|
||||||
|
<div class="col-chat" id="chatCol">
|
||||||
|
<iframe src="https://irc.crans.org/web/?join=stream_{{.path}}&nick=viewer&password=&realname=Viewer" title="Chat"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/ovenplayer/ovenplayer.js"></script>
|
||||||
|
<script>
|
||||||
|
// Toggle chat
|
||||||
|
const chatToggle = document.getElementById("chatToggle")
|
||||||
|
chatToggle.addEventListener("click", function () {
|
||||||
|
const chatCol = document.getElementById("chatCol")
|
||||||
|
if (chatCol.style.display === "none") {
|
||||||
|
chatCol.style.display = "block"
|
||||||
|
chatToggle.textContent = "»"
|
||||||
|
} else {
|
||||||
|
chatCol.style.display = "none"
|
||||||
|
chatToggle.textContent = "«"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create player
|
||||||
|
player = OvenPlayer.create("player", {
|
||||||
|
autoStart: true,
|
||||||
|
mute: true,
|
||||||
|
expandFullScreenUI: true,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
"file": "wss://SITE_HOSTNAME/play/{{.path}}",
|
||||||
|
"type": "webrtc",
|
||||||
|
"label": "WebRTC Source"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
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>
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue
Block a user