mirror of
				https://gitlab.crans.org/nounous/ghostream.git
				synced 2025-11-04 15:42:26 +01:00 
			
		
		
		
	Initial Golang project
This commit is contained in:
		
							
								
								
									
										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}}
 | 
			
		||||
		Reference in New Issue
	
	Block a user