14 changed files with 841 additions and 0 deletions
@ -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%; |
||||
} |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.3 KiB |
@ -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> |
@ -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'); |
||||
}); |
||||
} |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 32 KiB |
@ -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" |
After Width: | Height: | Size: 104 KiB |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue