mirror of
https://gitlab.crans.org/nounous/ghostream.git
synced 2025-07-28 20:51:38 +02:00
Compare commits
13 Commits
h264-reade
...
cfcde6f530
Author | SHA1 | Date | |
---|---|---|---|
cfcde6f530 | |||
28ef6a5526 | |||
5ad8a69c4c | |||
d334556d2b | |||
9625cba5e1 | |||
e74acf04f7 | |||
2085d13c0d | |||
85a5606291 | |||
33f86a0742 | |||
11d89c6950 | |||
c9a2d5b359 | |||
ee927c5b8f | |||
955364a5fc |
100
docs/Server-docker.xml
Normal file
100
docs/Server-docker.xml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?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>
|
||||||
|
</Publishers>
|
||||||
|
</Bind>
|
||||||
|
|
||||||
|
<VirtualHosts>
|
||||||
|
<VirtualHost>
|
||||||
|
<Name>default</Name>
|
||||||
|
<Domain>
|
||||||
|
<Names>
|
||||||
|
<Name>*</Name>
|
||||||
|
</Names>
|
||||||
|
</Domain>
|
||||||
|
<Applications>
|
||||||
|
<Application>
|
||||||
|
<Name>app</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>
|
||||||
|
<Video>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Video>
|
||||||
|
<Audio>
|
||||||
|
<Bypass>true</Bypass>
|
||||||
|
</Audio>
|
||||||
|
</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>5</SegmentDuration>
|
||||||
|
<SegmentCount>2</SegmentCount>
|
||||||
|
<CrossDomain>
|
||||||
|
<Url>*</Url>
|
||||||
|
</CrossDomain>
|
||||||
|
</HLS>
|
||||||
|
</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(`/app/`)"
|
||||||
|
- "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(`/app/{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"
|
||||||
|
@ -49,6 +49,7 @@ forwarding:
|
|||||||
# - rtmp://live-cdg.twitch.tv/app/STREAM_KEY
|
# - rtmp://live-cdg.twitch.tv/app/STREAM_KEY
|
||||||
# - rtmp://a.rtmp.youtube.com/live2/STREAM_KEY
|
# - rtmp://a.rtmp.youtube.com/live2/STREAM_KEY
|
||||||
# - /home/ghostream/lives/%name/live-%Y-%m-%d-%H-%M-%S.flv
|
# - /home/ghostream/lives/%name/live-%Y-%m-%d-%H-%M-%S.flv
|
||||||
|
# - rtmp://ovenmediaengine:1915/app/demo # For player
|
||||||
|
|
||||||
## Prometheus monitoring ##
|
## Prometheus monitoring ##
|
||||||
# Expose a monitoring endpoint for Prometheus
|
# Expose a monitoring endpoint for Prometheus
|
||||||
@ -164,7 +165,7 @@ web:
|
|||||||
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.
|
||||||
|
@ -77,7 +77,7 @@ func New() *Config {
|
|||||||
ViewersCounterRefreshPeriod: 20000,
|
ViewersCounterRefreshPeriod: 20000,
|
||||||
},
|
},
|
||||||
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"},
|
||||||
|
@ -74,8 +74,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...)
|
||||||
|
|
||||||
|
105
web/static/js/ovenplayer.js
Normal file
105
web/static/js/ovenplayer.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { ViewerCounter } from "./modules/viewerCounter.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize viewer page
|
||||||
|
*
|
||||||
|
* @param {String} stream
|
||||||
|
* @param {Number} viewersCounterRefreshPeriod
|
||||||
|
* @param {String} posterUrl
|
||||||
|
*/
|
||||||
|
export function initViewerPage(stream, 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 + "/app/" + stream,
|
||||||
|
"type": "webrtc",
|
||||||
|
"label": " WebRTC - Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hls",
|
||||||
|
"file": "https://" + window.location.host + "/app/" + stream + "_bypass/playlist.m3u8",
|
||||||
|
"label": " HLS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
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 */
|
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 */
|
@ -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,13 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
|
<script src="/static/ovenplayer/ovenplayer.js"></script>
|
||||||
|
<script src="/static/js/ovenplayer.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { initViewerPage } from "/static/js/viewer.js";
|
// import { initViewerPage } from "/static/js/viewer.js";
|
||||||
|
import { initViewerPage } from "/static/js/ovenplayer.js";
|
||||||
|
|
||||||
// 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 +50,6 @@
|
|||||||
"{{$value}}",
|
"{{$value}}",
|
||||||
{{end}}
|
{{end}}
|
||||||
]
|
]
|
||||||
initViewerPage(stream, stunServers, viewersCounterRefreshPeriod)
|
initViewerPage(stream, /*stunServers,*/ viewersCounterRefreshPeriod, {{.Cfg.PlayerPoster}})
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Reference in New Issue
Block a user