Astroport/IPFS video streaming alpha

This commit is contained in:
qo-op 2021-04-07 23:01:54 +02:00
parent 55708f8067
commit 29b77c5971
14 changed files with 841 additions and 0 deletions

View File

@ -0,0 +1,261 @@
html, body {
width: 100% !important;
height: 100% !important;
overflow: hidden;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: #000000;
font: normal 18px/1.4 nimbus sans l,helvetica neue,arial,sans-serif;
}
h2 {
margin-top: 0;
margin-bottom: 0.35em;
font-weight: normal;
}
p {
margin-top: 0;
margin-bottom: 1em;
}
/* Variables */
.color-accent {
color: #ff3f00;
}
.bg-white {
background: #ffffff;
}
/* Layout */
.fixed {
position: fixed;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.none {
display: none;
}
.block {
display: block;
}
.flex {
display: flex;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-direction-column {
flex-direction: column;
}
.flex-align-baseline {
align-items: baseline;
}
.flex-align-center {
align-items: center;
}
.flex-justify-center {
justify-content: center;
}
.flex-justify-space-between {
justify-content: space-between;
}
.w-100 {
width: 100%;
}
.pt-s {
padding-top: 1em;
}
.mt-0 {
margin-top: 0;
}
.mt-1 {
margin-top: 1em;
}
.mr-1 {
margin-right: 1em;
}
/* Buttons */
.button,
button {
display: inline-flex;
align-items: baseline;
justify-content: center;
padding: 1em 2em 1em;
color: #000000;
text-align: center;
font-family: nimbus sans l,helvetica neue,arial,sans-serif;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
background-color: #ffe100;
border: 0;
cursor: pointer;
line-height: 1em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background 150ms ease-in-out, border-color 150ms ease-in-out, color 150ms ease-in-out;
}
.button:hover,
button:hover,
.button:focus,
button:focus {
color: #ffffff;
background-color: #ff3f00;
border-color: #ff3f00;
text-decoration: none;
}
.button.button-primary,
button.button-primary {
border: 7px solid #ff3f00;
background-color: #ffe100;
}
.button.button-primary:hover,
button.button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus {
color: #ffffff;
background-color: #ff3f00;
border-color: #ff3f00;
}
.compact {
font-size: 0.85em;
line-height: 1.5em;
padding: 5px 10px;
}
/* Live Stream Container */
.stream-container {
position: relative;
min-height: 350px;
margin-bottom: 1em;
height: 100%;
}
.stream-selector {
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100%;
text-align: center;
flex-direction: column;
}
.selector-option {
margin-top: -62px;
}
.stream-option {
padding: 0.75em;
margin: 0 10px;
flex-direction: column;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0px 5px 16px 0 rgba(0, 0, 0, 0.1);
}
.stream-option:hover, .stream-option:focus {
background: #ffffff;
}
.stream-option:hover .button-label, .stream-option:focus .button-label {
text-decoration: underline;
}
.stream-option .stream-option-graphic {
width: 130px;
pointer-events: none;
}
.stream-option[disabled] {
cursor: not-allowed;
opacity: 0.25;
}
.stream-option[disabled]:hover .button-label, .stream-option[disabled]:focus .button-label {
text-decoration: none;
}
.stream-option[disabled] .stream-option-graphic {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.stream-message {
max-width: 75%;
margin: 0 auto;
font-size: 0.85em;
}
.stream-option-title-helper {
top: 100%;
width: 225px;
font-size: 0.85em;
line-height: 1.2em;
}
.stream-option-wrapper:hover .stream-option-title-helper {
display: block;
}
.stream-refresh {
cursor: pointer;
margin-top: 1em;
background: #000000;
border-color: #000000;
color: #ffffff;
font-size: 0.85em;
padding: 15px 20px;
}
.stream-refresh:hover {
background-color: #000000;
border-color: #000000;
}
.button-label {
color: #000000;
margin-top: 1em;
}
.share-container {
z-index: 5;
}
#loadingTitle {
font-weight: normal;
margin-top: 0;
}
#live {
height: 100%;
}

1
www/video-player/css/video-js.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,14 @@
<svg class="loader-animation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="black">
<path transform="translate(-8 0)" d="M0 12 V20 H8 V12z">
<animateTransform attributeName="transform" type="translate" values="-8 0; 2 0; 2 0;" dur="0.8s" repeatCount="indefinite" begin="0" keytimes="0;.25;1" keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8" calcMode="spline" />
</path>
<path transform="translate(2 0)" d="M0 12 V20 H8 V12z">
<animateTransform attributeName="transform" type="translate" values="2 0; 12 0; 12 0;" dur="0.8s" repeatCount="indefinite" begin="0" keytimes="0;.35;1" keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8" calcMode="spline" />
</path>
<path transform="translate(12 0)" d="M0 12 V20 H8 V12z">
<animateTransform attributeName="transform" type="translate" values="12 0; 22 0; 22 0;" dur="0.8s" repeatCount="indefinite" begin="0" keytimes="0;.45;1" keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8" calcMode="spline" />
</path>
<path transform="translate(24 0)" d="M0 12 V20 H8 V12z">
<animateTransform attributeName="transform" type="translate" values="22 0; 32 0; 32 0;" dur="0.8s" repeatCount="indefinite" begin="0" keytimes="0;.55;1" keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8" calcMode="spline" />
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>IPFS Live Streaming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="css/video-js.min.css" rel="stylesheet">
<link href="css/common.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="share-container absolute w-100 flex">
<button class="share-tweet compact">Share on Twitter</button>
<button class="share-link compact">Share link</button>
<input type="text" class="compact" style="flex:1; text-overflow: ellipsis" id="link" />
</div>
<div class="stream-container">
<video id="live" class="video-js vjs-default-skin vjs-big-play-centered vjs-fill" controls preload autoplay loop>
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video</p>
</video>
<div class="stream-selector absolute flex flex-justify-center flex-align-center bg-white" id="streamSelector">
<div id="selectStream" class="selector-option">
<h2 id="selectingTitle">Select source</h2>
<div class="stream-selector-options flex flex-justify-center flex-align-center">
<div class="stream-option-wrapper flex relative flex-justify-center" id="ipfsStream">
<button class="stream-option flex-justify-center flex-align-center ipfs-stream bg-white">
<img class="stream-option-graphic" src="graphics/ipfs-icon.svg" alt="IPFS graphic" />
<span class="button-label mt-1 color-accent">IPFS</span>
</button>
<div class="stream-option-title-helper none absolute pt-s">
<p>Play the stream through a peer-to-peer hypermedia protocol. More at <a href="https://ipfs.io" target="_blank">ipfs.io</a></p>
</div>
</div>
<div class="stream-option-wrapper flex relative flex-justify-center" id="httpStream">
<button class="stream-option flex-justify-center flex-align-center http-stream bg-white">
<img class="stream-option-graphic" src="graphics/http-icon.svg" alt="Media server graphic" />
<span class="button-label mt-1 color-accent">HTTP</span>
</button>
<div class="stream-option-title-helper none absolute pt-s">
<p>Play the stream through a media server over HTTP</p>
</div>
</div>
</div>
</div>
<div id="loadingStream" class="selector-option" style="display:none;">
<img class="loader-animation" src="graphics/loader-animation.svg" alt="Animated loading graphic" />
<h3 id="loadingTitle" class="mt-0">Locating stream...</h3>
<div class="stream-message" id="msg"></div>
</div>
</div>
</div>
<script src="js/vendor/video.min.js"></script>
<script src="js/common.js"></script>
</body>
</html>

View File

@ -0,0 +1,264 @@
// IPFS config
var ipfs_gateway = '__IPFS_GATEWAY__'; // IPFS gateway
// Live stream config
var m3u8_ipfs = 'live.m3u8'; // HTTP or local path to m3u8 file containing IPFS content
//m3u8_ipfs = '__IPFS_GATEWAY__/ipns/__IPFS_ID_ORIGIN__'; // IPNS path to m3u8 file containing IPFS content (uncomment to enable)
var m3u8_http_urls = [__M3U8_HTTP_URLS__]; // HTTP or local paths to m3u8 file containing HTTP content (optional)
// Video sharing links config
var date = new Date().toLocaleDateString("en-CA", {timeZone: "America/Toronto"}); // Current date (default to American/Toronto)
var rootURL = window.location.href.split('?')[0]; // Root URL used in sharing links
// Process URL params
function getURLParam(key) {
return new URLSearchParams(window.location.search).get(key);
}
var ipfs_gw = getURLParam('gw'); // Set custom IPFS gateway
if (getURLParam('m3u8'))
var m3u8_ipfs = getURLParam('m3u8'); // Set m3u8 file URL to override IPFS live stream
var vod_ipfs = getURLParam('vod') || getURLParam('ipfs'); // Set IPFS content hash of mp4 file to play IPFS on-demand video stream ('ipfs' for backward compatability)
var start_from = getURLParam("from"); // Set IPFS content hash or timecode to start video playback from
// Configure default playback behaviour
var stream_type = 'application/x-mpegURL'; // Type of video stream
var stream_url_ipfs = m3u8_ipfs; // Source of IPFS video stream
var stream_urls_http = m3u8_http_urls; // Source of HTTP video stream
if (ipfs_gw) {
ipfs_gateway = ipfs_gw;
}
if (vod_ipfs) {
stream_type = 'video/mp4';
stream_url_ipfs = ipfs_gateway + '/ipfs/' + vod_ipfs;
stream_urls_http = [];
document.getElementById('selectingTitle').innerHTML = 'Select recorded stream source';
}
// If start_from is not a number it's probably an IPFS hash so calculate to correct start_from
var hash="";
if (start_from && +start_from != start_from) {
hash = start_from;
// Remove start_from value since the hash may not be in the list
start_from = undefined;
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
file = this.response;
fileline = file.split("\n");
counter = 0;
// Loop through entries in the file
for (var a = 0; a < fileline.length; a++) {
// Look for EXTINF tags that describe the length of the chunk
if (fileline[a].indexOf("EXTINF:") > 0) {
// Parse out the length of the chunk
var number = fileline[a].substring(fileline[a].indexOf("EXTINF:") + 7);
number = number.substring(0, number.length - 1);
// Skip over chunk hash information
a++;
if (fileline[a].indexOf(hash) > 0) {
// If hash is found set the start_from to the counter and exit;
start_from = counter;
return;
}
// Add chunk length to counter
counter = counter + parseFloat(number);
}
}
}
};
xmlhttp.open("GET", m3u8_ipfs, true);
xmlhttp.send();
}
// Function to get hash from timeindex
function getHashFromTime(timeindex) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", m3u8_ipfs, false);
xmlhttp.send();
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
file = xmlhttp.response;
fileline = file.split("\n");
counter = 0;
hash = "";
// Loop through entries in the file
for (var a = 0; a < fileline.length; a++) {
// Look for EXTINF tags that describe the length of the chunk
if (fileline[a].indexOf("EXTINF:") > 0) {
// Parse out the length of the chunk
var number = fileline[a].substring(fileline[a].indexOf("EXTINF:") + 7);
number = number.substring(0, number.length - 1);
counter = counter + parseFloat(number);
// Parse out current hash
var hash = fileline[a+1].substring(fileline[a+1].lastIndexOf("/") + 1);
// Check if the current counter is larger than timeindex requested
if (counter > timeindex) return hash;
// Skip over chunk hash information
a++;
}
}
}
return "";
}
// Configure video player
var live = videojs('live', { liveui: true });
// Override native player for platform and browser consistency
videojs.options.html5.nativeAudioTracks = false;
videojs.options.html5.nativeVideoTracks = false;
videojs.options.hls.overrideNative = true;
function httpStream() {
live.src({
src: stream_urls_http[Math.floor(Math.random() * m3u8_http_urls.length)],
type: stream_type
});
loadStream();
}
// Counter to track video playback state
var streamState = 0;
function ipfsStream() {
live.src({
src: stream_url_ipfs,
type: stream_type
});
loadStream();
// Start playback from timecode if exists
if (vod_ipfs && start_from && +start_from == start_from) {
setTimeout(function() {
live.currentTime(start_from);
}, 1);
}
videojs.Hls.xhr.beforeRequest = function(options) {
// When .m3u8 is loaded, start playback and transition to streamState = 1
if (options.uri.indexOf('.m3u8') > 0) {
if (!streamState) {
live.play();
streamState = 1;
}
}
if (options.uri.indexOf('/ipfs/') > 0) {
document.getElementById('loadingTitle').innerHTML = 'Located stream via IPFS';
document.getElementById('msg').innerHTML = 'Downloading video content...';
// Use specified IPFS gateway by replacing it in the uri
options.uri = ipfs_gateway + options.uri.substring(options.uri.indexOf('/ipfs/'));
// Wait for two .ts chunks to be loaded before applying seek action
if (streamState < 3) {
streamState++;
if (streamState == 3) {
if (!start_from) {
// Seek to live after waiting 1 s
setTimeout(function() { live.liveTracker.seekToLiveEdge(); }, 1);
} else {
// Seek to start_from time after waiting 1 s
setTimeout(function() { live.currentTime(start_from); }, 1);
}
}
}
}
if (options.uri.indexOf('/ipns/') > 0) {
document.getElementById('loadingTitle').innerHTML = 'Located stream via IPFS';
document.getElementById('msg').innerHTML = 'Downloading video content...';
options.uri = ipfs_gateway + options.uri.substring(options.uri.indexOf('/ipns/'));
}
console.debug(options.uri);
return options;
};
}
function loadStream() {
document.getElementById('loadingStream').style.display = 'block';
document.getElementById('selectStream').style.display = 'none';
}
document.querySelector('.ipfs-stream').addEventListener('click', function(event) {
ipfsStream();
});
document.querySelector('.http-stream').addEventListener('click', function(event) {
httpStream();
});
live.metadata = 'none';
live.on('loadedmetadata', function() {
document.getElementById('streamSelector').style.display = 'none';
});
live.on('loadeddata', function(event) {
console.debug(event);
});
var refreshButton = document.createElement('button');
refreshButton.className = 'button button-primary compact stream-refresh';
refreshButton.innerHTML = 'Refresh page and try again';
refreshButton.addEventListener('click', function() {
window.location.reload(true);
});
live.on('error', function(event) {
console.debug(this.error());
document.getElementById('loadingTitle').innerHTML = 'Unable to load video stream';
document.querySelector('.loader-animation').style.display = 'none';
document.getElementById('msg').innerHTML = this.error().message;
document.getElementById('loadingStream').appendChild(refreshButton);
});
if (!stream_urls_http || !Array.isArray(stream_urls_http) || (stream_urls_http.length === 0)) {
document.querySelector('.http-stream').setAttribute('disabled', 'disabled');
}
// Video sharing links
function getShareLink(key) {
if (vod_ipfs) {
return `${rootURL}?vod=${vod_ipfs}&from=${live.currentTime()}`;
}
var m3u8 = getURLParam('m3u8');
if (!m3u8) {
m3u8 = `live-${date}.m3u8`;
}
var bookmark = getHashFromTime(live.currentTime());
return `${rootURL}?m3u8=${m3u8}&from=${bookmark}`;
}
setInterval(function () {
var link = document.getElementById('link');
link.value = getShareLink();
}, 5000);
var shareTweet = document.querySelector('.share-tweet');
var shareLink = document.querySelector('.share-link');
if (shareTweet) {
shareTweet.addEventListener('click', function() {
var link = document.getElementById('link');
link.value = getShareLink();
const tweetURL = link.value;
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(tweetURL)}`);
});
}
if (shareLink) {
shareLink.addEventListener('click', function() {
var link = document.getElementById('link');
link.value = getShareLink();
link.select();
link.setSelectionRange(0, 99999); // For mobile devices
document.execCommand('copy');
alert('Link copied to clipboard');
});
}

20
www/video-player/js/vendor/video.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
zen/stream/audio.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

58
zen/stream/install.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -e
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Enable camera on the Raspberry Pi
# sudo "$BASE_DIR/enable-camera.sh"
# Install ffmpeg and supporting tools
sudo apt-get install -y ffmpeg lsof inotify-tools nginx
# Copy placeholder for audio-only streams
cp "$BASE_DIR/audio.jpg" "$HOME/audio.jpg"
# Add user to be able to modify nginx directories
sudo usermod -a -G "$USER" www-data
sudo chmod g+rw /var/www/html
# TODO: why is this needed?
sudo chmod a+rw /var/www/html
sudo cp -f "$BASE_DIR/process-stream.sh" /usr/bin/process-stream.sh
sudo cp -f "$BASE_DIR/process-stream.service" /etc/systemd/system/process-stream.service
sudo systemctl daemon-reload
sudo systemctl enable process-stream
########################################################################
exit 0
########################################################################
### REWRITE NEEDED
########################################################################
# Add hourly job to clear out old data
# echo "41 * * * * $USER /usr/local/bin/ipfs repo gc" | sudo tee --append /etc/crontab
# Install the ipfs video player
mkdir "$BASE_DIR/tmp"
current_dir="$(pwd)"
git clone https://github.com/tomeshnet/ipfs-live-streaming.git "$BASE_DIR/tmp/ipfs-live-streaming"
cd "$BASE_DIR/tmp/ipfs-live-streaming"
git checkout b9be352582317e5336ddd7183ecf49042dafb33e
cd "$current_dir"
VIDEO_PLAYER_PATH="$BASE_DIR/tmp/ipfs-live-streaming/terraform/shared/video-player"
sed -i s#__IPFS_GATEWAY_SELF__#/ipfs/# "$VIDEO_PLAYER_PATH/js/common.js"
sed -i s#__IPFS_GATEWAY_ORIGIN__#https://ipfs.io/ipfs/# "$VIDEO_PLAYER_PATH/js/common.js"
IPFS_ID=$(ipfs id | grep ID | head -n 1 | awk -F\" '{print $4}')
sed -i "s#live.m3u8#/ipns/$IPFS_ID#" "$VIDEO_PLAYER_PATH/js/common.js"
sed -i s#__M3U8_HTTP_URLS__#\ # "$VIDEO_PLAYER_PATH/js/common.js"
cp -r "$VIDEO_PLAYER_PATH" /var/www/html/video-player
rm -rf "$BASE_DIR/tmp"
# Add entry into nginx home screen
APP="<div class='app'><h2>IPFS Pi Stream Player</h2>IPFS Video player for Pi Stream. <br />M3U8 Stream located <a href='/ipns/$IPFS_ID'>over ipns</a> <br/><a href='/video-player/'>Go </a> and play with built in video player</div>"
sudo sed -i "s#<\!--APPLIST-->#$APP\n<\!--APPLIST-->#" "/var/www/html/index.html"

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -0,0 +1,16 @@
[Unit]
Description=Service to process RTMP stream
Wants=network.target
After=ipfs.service
[Service]
Type=simple
User=pi
Group=pi
ExecStart=/usr/bin/process-stream.sh
ExecStop=/bin/kill -s QUIT $MAINPID
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target

129
zen/stream/process-stream.sh Executable file
View File

@ -0,0 +1,129 @@
#!/usr/bin/env bash
HLS_TIME=40
M3U8_SIZE=3
IPFS_GATEWAY="http://127.0.0.1:8181"
# Load settings
# Prepare Pi Camera
# sudo modprobe bcm2835-v4l2
# sudo v4l2-ctl --set-ctrl video_bitrate=100000
function startFFmpeg() {
while true; do
mv ~/ffmpeg.log ~/ffmpeg.1
echo 1 > ~/stream-reset
# Stream WebCamera
ffmpeg -f video4linux2 -video_size 1280x720 -framerate 30 -i /dev/video0 -f alsa -i hw:0 -hls_time "${HLS_TIME}" "${what}.m3u8" > ~/ffmpeg.log 2>&1
## MORE SOURCES
# http://4youngpadawans.com/stream-camera-video-and-audio-with-ffmpeg/
## STILL BAD :: ffmpeg -f v4l2 -i /dev/video0 -f alsa -i hw:0 -profile:v high -pix_fmt yuvj420p -level:v 4.1 -preset ultrafast -tune zerolatency -vcodec libx264 -r 10 -b:v 512k -s 640x360 -acodec aac -strict -2 -ac 2 -ab 32k -ar 44100 -f mpegts -flush_packets 0 -hls_time "${HLS_TIME}" "${what}.m3u8" > ~/ffmpeg.log 2>&1
################ GOOD ?
# Stream FM Station from a SDR module (see contrib/pi-stream to install drivers)
# Frequency ends in M IE 99.9M
# rtl_fm -f 99.9M -M fm -s 170k -A std -l0 -E deemp -r 44.1k | ffmpeg -r 15 -loop 1 -i ../audio.jpg -f s16le -ac 1 -i pipe:0 -c:v libx264 -tune stillimage -preset ultrafast -hls_time "${HLS_TIME}" "${what}.m3u8" > ~/ffmpeg 2>&1
sleep 0.5
done
}
# Create directory for HLS content
currentpath="$HOME/live"
sudo umount "${currentpath}"
rm -rf "${currentpath}"
mkdir "${currentpath}"
sudo mount -t tmpfs tmpfs "${currentpath}"
# shellcheck disable=SC2164
cd "${currentpath}"
what="$(date +%Y%m%d%H%M)-LIVE"
# Start ffmpeg in background
startFFmpeg &
while true; do
#TODO# Fix this one
# shellcheck disable=SC2086,SC2012
nextfile=$(ls -tr ${what}*.ts 2>/dev/null | head -n 1)
if [ -n "${nextfile}" ]; then
# Check if the next file on the list is still being written to by ffmpeg
if lsof "${nextfile}" | grep -1 ffmpeg; then
# Wait for file to finish writing
# If not finished in 45 seconds something is wrong, timeout
inotifywait -e close_write "${nextfile}" -t ${HLS_TIME}
fi
# Grab the timecode from the m3u8 file so we can add it to the log
timecode=$(grep -B1 "${nextfile}" "${what}.m3u8" | head -n1 | awk -F : '{print $2}' | tr -d ,)
attempts=5
until [[ "${timecode}" || ${attempts} -eq 0 ]]; do
# Wait and retry
sleep 0.5
timecode=$(grep -B1 "${nextfile}" "${what}.m3u8" | head -n1 | awk -F : '{print $2}' | tr -d ,)
attempts=$((attempts-1))
done
if ! [[ "${timecode}" ]]; then
# Set approximate timecode
timecode="${HLS_TIME}.000000"
fi
reset_stream=$(cat ~/stream-reset)
reset_stream_marker=''
if [[ ${reset_stream} -eq '1' ]]; then
reset_stream_marker=" #EXT-X-DISCONTINUITY"
fi
echo 0 > ~/stream-reset
# Current UTC date for the log
time=$(date "+%F-%H-%M-%S")
echo "Add ts file to IPFS"
ret=$(ipfs add --pin=false "${nextfile}" 2>/dev/null > ~/tmp.txt; echo $?)
attempts=5
until [[ ${ret} -eq 0 || ${attempts} -eq 0 ]]; do
# Wait and retry
sleep 0.5
ret=$(ipfs add --pin=false "${nextfile}" 2>/dev/null > ~/tmp.txt; echo $?)
echo "$attempts RETRY"
attempts=$((attempts-1))
done
if [[ ${ret} -eq 0 ]]; then
# Update the log with the future name (hash already there)
echo "$(cat ~/tmp.txt) ${time}.ts ${timecode}${reset_stream_marker}" >> ~/process-stream.log
# Remove nextfile and tmp.txt
rm -f "${nextfile}" ~/tmp.txt
# Write the m3u8 file with the new IPFS hashes from the log
totalLines="$(wc -l ~/process-stream.log | awk '{print $1}')"
sequence=0
if ((totalLines>M3U8_SIZE)); then
sequence=$((totalLines-M3U8_SIZE))
fi
{
echo "#EXTM3U"
echo "#EXT-X-VERSION:3"
echo "#EXT-X-TARGETDURATION:${HLS_TIME}"
echo "#EXT-X-MEDIA-SEQUENCE:${sequence}"
} > current.m3u8
tail -n ${M3U8_SIZE} ~/process-stream.log | awk '{print $6"#EXTINF:"$5",\n'${IPFS_GATEWAY}'/ipfs/"$2}' | sed 's/#EXT-X-DISCONTINUITY#/#EXT-X-DISCONTINUITY\n#/g' >> current.m3u8
echo 'Add m3u8 file to IPFS and IPNS publish'
m3u8hash=$(ipfs add current.m3u8 | awk '{print $2}')
ipfs name publish --key='star_1' --timeout=5s "${m3u8hash}" &
# Copy files to web server
cp current.m3u8 /var/www/html/live.m3u8
cp ~/process-stream.log /var/www/html/live.log
fi
else
sleep 5
fi
done

19
zen/stream/uninstall.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
## TO CONTROL & REWRITE
exit 0
set -e
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
sudo systemctl stop process-stream
sudo systemctl disable process-stream
sudo rm -f /usr/bin/process-stream.sh
sudo rm -f /etc/systemd/system/process-stream.service
sudo systemctl daemon-reload
# Remove ffmpeg and supporting tools
sudo apt-get -y remove ffmpeg lsof inotify-tools
# Revert permissions
sudo chmod 755 /var/www/html
sed -i "/ipfs repo gc/d" | sudo tee --append /etc/crontab