Astroport/IPFS video streaming alpha
This commit is contained in:
parent
55708f8067
commit
29b77c5971
|
@ -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
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 |
|
@ -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 |
|
@ -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
Binary file not shown.
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"
|
Binary file not shown.
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