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