Compare commits

..

13 Commits

Author SHA1 Message Date
e854cdf234 Moved portAdded support for rear and front camera 2023-10-31 20:56:20 +01:00
8af2650aca Reduced 2023-10-31 19:57:42 +01:00
470147c5cd Added tools 2023-10-31 19:44:06 +01:00
4d4591fb2d Added readme 2023-09-21 12:47:26 +02:00
e5b57dd3d7 Handle error on audio dispatching 2023-09-21 12:31:40 +02:00
d5c8fcd055 Media stream 2023-09-21 12:19:11 +02:00
980d6822d0 support audio 2023-09-21 12:16:12 +02:00
68b20a8b4f Added log 2023-09-21 12:04:11 +02:00
c5fb07f6da fix 2023-09-21 11:56:20 +02:00
c4f1e4d8c0 Added video support 2023-09-21 11:52:30 +02:00
d62722ce53 Added cache control 2023-09-21 11:15:06 +02:00
3a3e008e90 Added page examples 2023-09-15 12:06:27 +02:00
74e8731fa4 Take full screen if width and height are not supplied 2023-09-15 12:02:27 +02:00
11 changed files with 237 additions and 39 deletions

View File

@ -1,5 +1,13 @@
# LOLED
```
apt install xorg fluxbox lightdm
```
Outil de projection sur l'ecran led du LOL
L'ecran lED du LOL est controlle par une carte PCI qui prend une portion de l'ecran pour en mapper chaque pixel sur l'ecran LED.
Avec LOLED : un serveur (`loled.js`) execute avec [Deno](https://deno.com/) et mets a disposition les elements suivants :
* Une page `/display` qui doit etre ouverte en plein ecran sur un navigateur interne
* Une page d'index `/` qui donne quelques details et instructions sur comment utiliser l'outil sur le WEB
* Un script `/js/grab-canvas.js` qui peut etre execute sur n'importe quel page du web dispose d'un element video ou canvas, le flux video du premier element trouve sera alors envoye a l'ecran
Techniquement, le systeme repose sur une connexion WebRTC et utilise le serveur comme serveur de signalisation. Sur la machine assocee a l'ecran LED nous avons demarre une session fluxbox avec LightDM. Flubox demarre un firefox automatquement qui se connecte au serveur en tant qu'affichage.

View File

@ -82,12 +82,20 @@ app.use((ctx, next) => {
ctx.response.headers.append("Access-Control-Allow-Headers", "*")
if(ctx.request.method == "OPTION"){
ctx.response.status = 200
ctx.response.status = 204
} else {
return next()
}
})
app.use((ctx, next) => {
if(ctx.request.url.pathname.startsWith("/_loled")){
ctx.response.headers.append("Cache-Control", "no-store")
}
return next()
})
// Static
app.use(async (ctx, next) => {
if(ctx.request.url.pathname.startsWith("/_loled"))

37
static/camera.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>LOLED - Camera</title>
</head>
<body>
<h1>LOLED</h1>
<button id="stream-camera">Use rear Camera</button>
<button id="stream-front-camera">Use front Camera</button>
<script type="module">
import {connectSomeStream} from "/js/rtc.js"
document.getElementById("stream-camera")
.addEventListener("click", async () => {
let stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment'
}
})
await connectSomeStream(stream)
})
document.getElementById("stream-front-camera")
.addEventListener("click", async () => {
let stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user'
}
})
await connectSomeStream(stream)
})
</script>
</body>
</html>

40
static/canvas.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>LOLED - Canvas</title>
</head>
<body>
<canvas></canvas>
<p>To send canvas of the page to LOLED click the following link :
<a id="link" href="">LOLED that canvas !</a>
</p>
<p>You can also drag this link in your bookmarks to call it on any page with a canvas. Like <a href="https://hydra.ojack.xyz/">Hydra</a> or <a href="https://topos.raphaelforment.fr/">Topos</a>.</p>
<script>
document.getElementById("link").href = `javascript:(function(){let s = document.createElement('script');s.src = '${new URL("/js/grab-canvas.js", document.documentElement.baseURI).toString()}';document.body.appendChild(s);})()`
</script>
<script>
const canvas = document.querySelector("canvas");
canvas.width = 500
canvas.height = 500
const ctx = canvas.getContext("2d");
let incr = 0;
function draw(){
ctx.fillStyle = `hsl(${incr}deg 100% 50%)`;
ctx.fillRect(-50, -50, 100, 100)
incr += 1;
requestAnimationFrame(draw)
}
ctx.translate(canvas.width/2, canvas.height/2),
draw()
</script>
</body>
</html>

25
static/css/style.css Normal file
View File

@ -0,0 +1,25 @@
:root {
font-family: monospace;
color: white;
background: black;
}
html, body {
margin: 0;
width: 100%;
min-height: 100vh;
box-sizing: border-box;
}
body {
padding: 15px;
}
a {
color: yellow;
}
hr {
border: none;
height: 2em;
}

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Display</title>
<title>LOLED - Display</title>
<style>
body {
@ -44,7 +44,7 @@
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.75;
opacity: 0.5;
}
</style>
</head>

View File

@ -3,37 +3,22 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>LOLED</title>
</head>
<body>
<canvas></canvas>
<p>To send canvas of the page to LOLED click the following link :
<a id="link" href="">LOLED that canvas !</a>
<h1>Loled</h1>
<hr>
<p>
Getting started with loled by streaming some video on the screen.
Multiple solutions exists:
</p>
<p>
<ul>
<li><a href="canvas.html">Use a canvas or a video from a website</a></li>
<li><a href="camera.html">Use your device camera</a></li>
<li><a href="screenshare.html">Share your device screen</a></li>
</ul>
</p>
<p>You can also drag this link in your bookmarks to call it on any page with a canvas</p>
<script>
document.getElementById("link").href = `javascript:(function(){let s = document.createElement('script');s.src = '${new URL("/js/grab-canvas.js", document.documentElement.baseURI).toString()}';document.body.appendChild(s);})()`
</script>
<script>
const canvas = document.querySelector("canvas");
canvas.width = 500
canvas.height = 500
const ctx = canvas.getContext("2d");
let incr = 0;
function draw(){
ctx.fillStyle = `hsl(${incr}deg 100% 50%)`;
ctx.fillRect(-50, -50, 100, 100)
incr += 1;
requestAnimationFrame(draw)
}
ctx.translate(canvas.width/2, canvas.height/2),
draw()
</script>
</body>
</html>

View File

@ -90,6 +90,6 @@ async function getRegistration(name){
const args = new URLSearchParams(location.search);
init(
args.get("name"),
parseInt(args.get("width")),
parseInt(args.get("height"))
parseInt(args.get("width") || window.innerWidth),
parseInt(args.get("height") || window.innerHeight)
)

View File

@ -2,9 +2,10 @@
const globalConnnection = Symbol("grab-canvas-connection")
const currentScriptSrc = document.currentScript.src;
const canvas = document.querySelector("canvas");
const sourceElt = document.querySelector("canvas,video");
console.log("Grabbing", sourceElt)
if(!canvas){
if(!sourceElt){
console.error("No canvas found on this page")
}
@ -22,10 +23,38 @@
const allCandidatesCollected = new Promise(res =>
conn.addEventListener("icecandidate", e => e.candidate == null && res() ))
const canvasStream = canvas.captureStream()
let captureFn = sourceElt.captureStream;
if(!captureFn){
captureFn = sourceElt.mozCaptureStream;
}
if(sourceElt instanceof HTMLCanvasElement){
captureFn = sourceElt.captureStream.bind(sourceElt)
} else {
if(sourceElt.mozCaptureStream){
captureFn = sourceElt.mozCaptureStream.bind(sourceElt)
} else {
captureFn = sourceElt.captureStream.bind(sourceElt)
}
}
const canvasStream = captureFn()
const canvasStreamTracks = canvasStream.getVideoTracks()
if(canvasStreamTracks.length > 0){
conn.addTrack(canvasStreamTracks[0], canvasStream)
} else {
throw new Error("Element don't habe video track")
}
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if(AudioContext){
const audioContext = new AudioContext();
audioContext.createMediaStreamSource(canvasStream)
.connect(audioContext.destination)
}
} catch(e){
console.warn(e)
}
const offer = await conn.createOffer();

42
static/js/rtc.js Normal file
View File

@ -0,0 +1,42 @@
export async function connectSomeStream(canvasStream){
const globalConnnection = Symbol("grab-canvas-connection")
const conn = new RTCPeerConnection({
iceServers: [
{urls: ["stun:stun.nextcloud.com:443"]}
]
});
const iceCandidates = []
conn.addEventListener("icecandidate", e => {
iceCandidates.push(e.candidate)
})
const allCandidatesCollected = new Promise(res =>
conn.addEventListener("icecandidate", e => e.candidate == null && res() ))
const canvasStreamTracks = canvasStream.getVideoTracks()
if(canvasStreamTracks.length > 0){
conn.addTrack(canvasStreamTracks[0], canvasStream)
} else {
throw new Error("Stream don't have video track")
}
const offer = await conn.createOffer();
await conn.setLocalDescription(offer);
await allCandidatesCollected
const res = await fetch(new URL("/_loled/grab-display", window.location), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
offer,
iceCandidates
})
})
const response = new RTCSessionDescription(await res.json());
conn.setRemoteDescription(response);
window[globalConnnection] = conn
}

24
static/screenshare.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>LOLED - Camera</title>
</head>
<body>
<h1>LOLED <small>Share your screen</small></h1>
<button id="stream-camera">Share your screen</button>
<script type="module">
import {connectSomeStream} from "/js/rtc.js"
document.getElementById("stream-camera")
.addEventListener("click", async () => {
let stream = await navigator.mediaDevices.getDisplayMedia({
video: true
})
await connectSomeStream(stream)
})
</script>
</body>
</html>