Merge pull request #1 from TheGreyDiamond/video-support

Adds equirectangular video support
This commit is contained in:
TheGreyDiamond
2022-01-04 20:39:49 +01:00
committed by GitHub
7 changed files with 392 additions and 62 deletions

View File

@@ -1,5 +1,5 @@
# open360viewer # open360viewer
open360viewer is an opensource 360° image viewer. It is based on electron and marzipano. It currently supports opening equirectangular 360° images. open360viewer is an opensource 360° media viewer. It is based on electron and marzipano. It currently supports opening equirectangular 360° images and videos.
## Getting started ## Getting started
Currently the viewer can only be used if you have nodeJs installed. Complete packaging is planned. Currently the viewer can only be used if you have nodeJs installed. Complete packaging is planned.
@@ -10,8 +10,8 @@ Currently the viewer can only be used if you have nodeJs installed. Complete pac
If you are developing you might want to use `npm run startDev` as it also builds all assets at each launch. If you are developing you might want to use `npm run startDev` as it also builds all assets at each launch.
## Features ## Features
- viewing equirectangular 360° images - viewing equirectangular 360° images
### WiP
- viewing equirectangular 360° videos - viewing equirectangular 360° videos
### WiP
### Planned features ### Planned features
- being able to flip through all images in a folder - being able to flip through all images in a folder
- show meta data - show meta data

View File

@@ -8,7 +8,7 @@ const {
} = require("electron"); } = require("electron");
const url = require("url"); const url = require("url");
const path = require("path"); const path = require("path");
const FileType = require("file-type");
let win; let win;
const isMac = process.platform === "darwin"; const isMac = process.platform === "darwin";
@@ -24,8 +24,14 @@ const template = [
dialog dialog
.showOpenDialog({ properties: ["openFile", "multiSelections"] }) .showOpenDialog({ properties: ["openFile", "multiSelections"] })
.then(function (data) { .then(function (data) {
console.log(data); if (data.canceled == false) {
win.webContents.send("FileData", data); FileType.fromFile(data.filePaths[0]).then((type) => {
data.type = type["mime"].split("/")[0];
win.webContents.send("FileData", data);
});
}
// console.log(await FileType.fromFile(data));
// win.webContents.send("FileData", data);
}); });
}, },
}, },
@@ -96,8 +102,10 @@ app.on("ready", function () {
dialog dialog
.showOpenDialog({ properties: ["openFile", "multiSelections"] }) .showOpenDialog({ properties: ["openFile", "multiSelections"] })
.then(function (data) { .then(function (data) {
console.log(data); FileType.fromFile(data.filePaths[0]).then((type) => {
win.webContents.send("FileData", data); data.type = type["mime"].split("/")[0];
win.webContents.send("FileData", data);
});
}); });
} else if (arg == "resize") { } else if (arg == "resize") {
// A really ugly hack to force the window to update, so the canvas shows up // A really ugly hack to force the window to update, so the canvas shows up

View File

@@ -12,7 +12,7 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .", "start": "electron .",
"startDev": "npm run buildCss && npm run minify && electron .", "startDev": "npm run buildCss && npm run minify && electron .",
"minify": "minify src/main.js > dist/main.js && minify src/ui_templates/index.html > dist/ui_templates/index.html && minify src/ui_templates/about.html > dist/ui_templates/about.html", "minify": "minify src/main.js > dist/main.js && minify src/ui_templates/index.html > dist/ui_templates/index.html && minify src/videoPlayer.css > dist/videoPlayer.css && minify src/videoPlayHandler.js > dist/videoPlayHandler.js && minify src/ui_templates/videoPlayer.html > dist/ui_templates/videoPlayer.html && minify src/ui_templates/about.html > dist/ui_templates/about.html",
"buildCss": "npx tailwindcss -i ./src/main.css -o ./dist/output.css", "buildCss": "npx tailwindcss -i ./src/main.css -o ./dist/output.css",
"buildCssWatch": "npx tailwindcss -i ./src/main.css -o ./dist/output.css --watch", "buildCssWatch": "npx tailwindcss -i ./src/main.css -o ./dist/output.css --watch",
"package": "electron-forge package", "package": "electron-forge package",
@@ -51,16 +51,17 @@
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@themesberg/flowbite": "^1.2.0", "@themesberg/flowbite": "^1.2.0",
"electron": "^16.0.5", "electron": "^16.0.5",
"file-type": "^16.5.3",
"marzipano": "^0.10.2" "marzipano": "^0.10.2"
}, },
"devDependencies": { "devDependencies": {
"minify": "^8.0.3",
"tailwindcss": "^3.0.8",
"@electron-forge/cli": "^6.0.0-beta.61", "@electron-forge/cli": "^6.0.0-beta.61",
"@electron-forge/maker-deb": "^6.0.0-beta.61", "@electron-forge/maker-deb": "^6.0.0-beta.61",
"@electron-forge/maker-rpm": "^6.0.0-beta.61", "@electron-forge/maker-rpm": "^6.0.0-beta.61",
"@electron-forge/maker-squirrel": "^6.0.0-beta.61", "@electron-forge/maker-squirrel": "^6.0.0-beta.61",
"@electron-forge/maker-zip": "^6.0.0-beta.61", "@electron-forge/maker-zip": "^6.0.0-beta.61",
"electron": "^16.0.5" "electron": "^16.0.5",
"minify": "^8.0.3",
"tailwindcss": "^3.0.8"
} }
} }

View File

@@ -8,7 +8,11 @@ ipcRenderer.on("FileData", function (event, data) {
document.getElementById("loadingBig").style.display = "block"; document.getElementById("loadingBig").style.display = "block";
document.getElementById("state").innerHTML = document.getElementById("state").innerHTML =
"Loading file. If this stays empty try another file."; "Loading file. If this stays empty try another file.";
loadImageFromSource(data.filePaths[0]); if (data.type == "image") {
loadImageFromSource(data.filePaths[0]);
} else if (data.type == "video") {
loadVideoFromSource(data.filePaths[0]);
}
} }
}); });
@@ -16,7 +20,7 @@ function openFile() {
ipcRenderer.sendSync("synchronous-message", "openFile"); ipcRenderer.sendSync("synchronous-message", "openFile");
} }
var viewer = new Marzipano.Viewer(document.getElementById("pano")); var viewer = new Marzipano.Viewer(document.getElementById("pano2"));
function newPano(path) { function newPano(path) {
var sourceIm = Marzipano.ImageUrlSource.fromString(path); var sourceIm = Marzipano.ImageUrlSource.fromString(path);
// Create scene. // Create scene.
@@ -28,9 +32,10 @@ function newPano(path) {
}); });
scene.switchTo(); scene.switchTo();
setTimeout(function () { setTimeout(function () {
scene.switchTo() scene.switchTo();
}, 20); }, 20);
ipcRenderer.sendSync("synchronous-message", "resize"); ipcRenderer.sendSync("synchronous-message", "resize");
document.getElementById("video-controls").style.display = "none";
} }
var geometry = new Marzipano.EquirectGeometry([{ width: 4000 }]); var geometry = new Marzipano.EquirectGeometry([{ width: 4000 }]);
@@ -41,3 +46,19 @@ var limiter = Marzipano.RectilinearView.limit.traditional(
(120 * Math.PI) / 180 (120 * Math.PI) / 180
); );
var view = new Marzipano.RectilinearView({ yaw: Math.PI }, limiter); var view = new Marzipano.RectilinearView({ yaw: Math.PI }, limiter);
function loadVideoFromSource(path) {
setTimeout(function () {
multiResVideo.setResolutionIndex(1, path, loadingDone);
}, 20);
}
function loadingDone(state) {
if (!state) {
document.getElementById("loadingBig").style.display = "none";
ipcRenderer.sendSync("synchronous-message", "resize");
document.getElementById("pano").style.display = "block";
document.getElementById("pano2").style.display = "none";
document.getElementById("video-controls").style.display = "block";
}
}

View File

@@ -2,26 +2,39 @@
<html class="dark"> <html class="dark">
<head> <head>
<meta charset="UTF-8">
<title>360 Viewer</title> <title>360 Viewer</title>
<script src="../../node_modules/marzipano/dist/marzipano.js"> <meta name="viewport"
</script> content="target-densitydpi=device-dpi, width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui" />
<link rel="stylesheet" href="../../dist/videoPlayer.css">
<link href="../output.css" rel="stylesheet"> <link href="../output.css" rel="stylesheet">
<link href="../../node_modules/@fortawesome/fontawesome-free/css/all.css" rel="stylesheet"> <link href="../../node_modules/@fortawesome/fontawesome-free/css/all.css" rel="stylesheet">
<script src="../../node_modules/@themesberg/flowbite/dist/flowbite.bundle.js"></script> <script src="../../node_modules/@themesberg/flowbite/dist/flowbite.bundle.js"></script>
</head> </head>
<body class="dark:bg-gray-800"> <body class="dark:bg-gray-800">
<div id="alert-1" class="flex p-4 mb-4 bg-red-100 rounded-lg dark:bg-red-200" role="alert" style="display: none;"> <div id="alert-1" class="flex p-4 mb-4 bg-red-100 rounded-lg dark:bg-red-200" role="alert" style="display: none;">
<svg class="flex-shrink-0 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg> <svg class="flex-shrink-0 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"></path>
</svg>
<div class="ml-3 text-sm font-medium text-red-700 dark:text-red-800"> <div class="ml-3 text-sm font-medium text-red-700 dark:text-red-800">
The provided image was invalid and could not be loaded. The provided image was invalid and could not be loaded.
</div> </div>
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-red-100 text-red-500 rounded-lg focus:ring-2 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 dark:bg-red-200 dark:text-red-600 dark:hover:bg-red-300" data-collapse-toggle="alert-1" aria-label="Close" onclick="document.getElementById('alert-1').style.display = 'none'"> <button type="button"
<span class="sr-only">Close</span> class="ml-auto -mx-1.5 -my-1.5 bg-red-100 text-red-500 rounded-lg focus:ring-2 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 dark:bg-red-200 dark:text-red-600 dark:hover:bg-red-300"
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg> data-collapse-toggle="alert-1" aria-label="Close"
onclick="document.getElementById('alert-1').style.display = 'none'">
<span class="sr-only">Close</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button> </button>
</div> </div>
<center> <center>
<h1 id="state" style="display: none;"></h1> <h1 id="state" style="display: none;"></h1>
@@ -48,19 +61,65 @@
<div id="pano"> <div id="pano">
</div> </div>
<div id="pano2">
</div>
<div class="video-controls" id="video-controls" style="display: none;">
<div class="control-btn play" id="play-pause">
<i class="fas fa-play play-icon"></i>
<i class="fas fa-pause pause-icon"></i>
</div>
<div class="control-btn sound" id="mute">
<i class="fas fa-volume-up sound-on"></i>
<i class="fas fa-volume-mute sound-off"></i>
</div>
<div class="time">
<h5 class="initial-time" id="current-time-indicator"></h5>
<div class="progress-wrapper" id="progress-background">
<div class="progress-bar">
<span class="progress-fill" id="progress-fill"></span>
</div>
</div>
<h5 class="end-time" id="duration-indicator"></h5>
</div>
</div>
</body> </body>
<script src="../../node_modules/marzipano/dist/marzipano.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/VideoAsset.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/EventEmitter.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/EventEmitterProxy.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/NullVideoElementWrapper.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/loadVideoInSync.js"></script>
<script src="../../dist/videoPlayHandler.js"></script>
<script src="../../node_modules/marzipano/demos/video-multi-res/interface.js"></script>
<script> <script>
const FileType = require("file-type");
document.getElementById("pano").style.display = "none"; document.getElementById("pano").style.display = "none";
document.getElementById("pano2").style.display = "none";
document.addEventListener('drop', (event) => { document.addEventListener('drop', (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
document.getElementById("fakeDropzone").style.display = "none"; document.getElementById("fakeDropzone").style.display = "none";
document.getElementById("loadingBig").style.display = "block"; document.getElementById("loadingBig").style.display = "block";
document.getElementById("state").innerHTML = "Loading file. If this stays empty try another file." document.getElementById("state").innerHTML = "Loading file. If this stays empty try another file."
const superFile = event.dataTransfer.files[0].path; const superFile = event.dataTransfer.files[0].path;
loadImageFromSource(superFile) FileType.fromFile(superFile).then((type) => {
type = type["mime"].split("/")[0];
if (type == "image") {
loadImageFromSource(superFile)
} else if (type == "video") {
loadVideoFromSource(superFile)
}
});
}); });
document.addEventListener('dragover', (e) => { document.addEventListener('dragover', (e) => {
@@ -80,18 +139,18 @@
testImage(path, testImage(path,
function (e, suc) { function (e, suc) {
if (suc == "success") { if (suc == "success") {
document.getElementById("pano").style.display = "block"; document.getElementById("pano2").style.display = "block";
setTimeout(function() { setTimeout(function () {
newPano(path) newPano(path)
}, 50); }, 50);
} }
else { else {
document.getElementById("state").innerHTML = "Load failed." document.getElementById("state").innerHTML = "Load failed."
document.getElementById('alert-1').style.display = 'block'; document.getElementById('alert-1').style.display = 'block';
document.getElementById("fakeDropzone").style.display = "block"; document.getElementById("fakeDropzone").style.display = "block";
document.getElementById("loadingBig").style.display = "none"; document.getElementById("loadingBig").style.display = "none";
document.getElementById("pano").style.display = "none"; document.getElementById("pano2").style.display = "none";
} }
}); });
} }
@@ -125,36 +184,5 @@
</script> </script>
<script src="../main.js"></script> <script src="../main.js"></script>
<style>
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
-webkit-touch-callout: none;
-ms-content-zooming: none;
}
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
#pano {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</html> </html>

80
src/videoPlayHandler.js Normal file
View File

@@ -0,0 +1,80 @@
// Create viewer.
var viewer = new Marzipano.Viewer(document.querySelector('#pano'));
// Create layer.
var asset = new VideoAsset();
var source = new Marzipano.SingleAssetSource(asset);
var geometry = new Marzipano.EquirectGeometry([ { width: 1 } ]);
var limiter = Marzipano.RectilinearView.limit.traditional(2560, 100*Math.PI/180);
var view = new Marzipano.RectilinearView(null, limiter);
var scene = viewer.createScene({ source: source, geometry: geometry, view: view, pinFirstLevel: false });
scene.switchTo({ transitionDuration: 0 });
var emitter = new EventEmitter();
var videoEmitter = new EventEmitterProxy();
function setResolutionIndex(index, vidScr, cb) {
cb = cb || function() {};
var videoSrc = vidScr;
var previousVideo = asset.video() && asset.video().videoElement();
loadVideoInSync(videoSrc, previousVideo, function(err, element) {
if (err) {
cb(err);
return;
}
if (previousVideo) {
previousVideo.pause();
previousVideo.volume = 0;
previousVideo.removeAttribute('src');
}
var VideoElementWrapper = NullVideoElementWrapper;
var wrappedVideo = new VideoElementWrapper(element);
asset.setVideo(wrappedVideo);
videoEmitter.setObject(element);
emitter.emit('change');
emitter.emit('resolutionChange');
cb();
});
}
var multiResVideo = {
layer: function() {
return scene.layer();
},
element: function() {
return asset.video() && asset.video().videoElement();
},
resolutions: function() {
return resolutions;
},
resolutionIndex: function() {
return currentState.resolutionIndex;
},
resolution: function() {
return currentState.resolutionIndex != null ?
resolutions[currentState.resolutionIndex] :
null;
},
setResolutionIndex: setResolutionIndex,
resolutionChanging: function() {
return currentState.resolutionChanging;
},
addEventListener: emitter.addEventListener.bind(emitter),
// events from proxy to videoElement
addEventListenerVideo: videoEmitter.addEventListener.bind(videoEmitter)
};

192
src/videoPlayer.css Normal file
View File

@@ -0,0 +1,192 @@
#pano {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#pano2 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* General layout */
.video-controls {
position: absolute;
bottom: 0;
width: 100%;
color: #fff;
overflow: hidden;
}
.control-btn,
.time {
height: 100%;
position: absolute;
box-size: border-box;
}
.video-controls {
height: 40px;
}
.play,
.sound,
.options {
width: 40px;
}
.play {
left: 0;
}
.sound {
left: 40px;
}
.options {
right: 0;
}
.resolution {
right: 40px;
}
.time {
position: absolute;
left: 80px;
right: 140px;
}
/* Control button style (play, mute, options, resolution) */
.control-btn {
background-color: rgb(103, 115, 131);
background-color: rgba(103, 115, 131, 0.8);
cursor: pointer;
transition: 0.3s all ease-in-out;
}
.control-btn:hover {
background-color: rgb(78, 88, 104);
background-color: rgba(78, 88, 104, 0.8);
}
/* Play, mute, options */
.play img,
.sound img,
.options img {
height: 66%;
width: 66%;
margin-top: 17%;
margin-left: 17%;
}
/* Progress bar */
.initial-time,
.progress-wrapper,
.end-time {
position: absolute;
}
.initial-time,
.end-time {
width: 50px;
}
.initial-time {
left: 14px;
}
.end-time {
right: 14px;
}
.progress-wrapper {
left: 78px;
right: 78px;
}
.initial-time,
.end-time {
text-align: center;
top: 12px;
}
.progress-wrapper {
padding: 15px 0;
cursor: pointer;
}
.progress-bar {
height: 10px;
background-color: rgb(103, 115, 131);
background-color: rgba(103, 115, 131, 0.8);
border-radius: 20px;
}
.progress-wrapper .progress-fill {
display: block;
height: 100%;
width: 0;
background-color: #eee;
border-radius: 20px;
}
.initial-time,
.end-time {
font-size: 14px;
}
.time {
background-color: rgb(58, 68, 84);
background-color: rgba(58, 68, 84, 0.8);
}
/* Show state */
.play-icon {
display: block;
font-size: x-large;
padding: 8px;
}
.pause-icon {
display: none;
font-size: x-large;
padding: 8px;
}
.video-playing .play-icon {
display: none;
}
.video-playing .pause-icon {
display: block;
}
.sound-on {
display: block;
font-size: x-large;
padding: 8px;
}
.sound-off {
display: none;
font-size: x-large;
padding: 8px;
}
.video-muted .sound-on {
display: none;
}
.video-muted .sound-off {
display: block;
}
.resolution-changing-indicator,
.resolution-modal-changing-indicator {
display: none;
}
.video-resolution-changing .resolution-changing-indicator,
.video-resolution-changing .resolution-modal-changing-indicator {
display: block;
}