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 # LOLED
``` Outil de projection sur l'ecran led du LOL
apt install xorg fluxbox lightdm
``` 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", "*") ctx.response.headers.append("Access-Control-Allow-Headers", "*")
if(ctx.request.method == "OPTION"){ if(ctx.request.method == "OPTION"){
ctx.response.status = 200 ctx.response.status = 204
} else { } else {
return next() return next()
} }
}) })
app.use((ctx, next) => {
if(ctx.request.url.pathname.startsWith("/_loled")){
ctx.response.headers.append("Cache-Control", "no-store")
}
return next()
})
// Static // Static
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
if(ctx.request.url.pathname.startsWith("/_loled")) 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> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Display</title> <title>LOLED - Display</title>
<style> <style>
body { body {
@ -44,7 +44,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
opacity: 0.75; opacity: 0.5;
} }
</style> </style>
</head> </head>

View File

@ -3,37 +3,22 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title>LOLED</title> <title>LOLED</title>
</head> </head>
<body> <body>
<canvas></canvas> <h1>Loled</h1>
<p>To send canvas of the page to LOLED click the following link : <hr>
<a id="link" href="">LOLED that canvas !</a> <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>
<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> </body>
</html> </html>

View File

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

View File

@ -2,9 +2,10 @@
const globalConnnection = Symbol("grab-canvas-connection") const globalConnnection = Symbol("grab-canvas-connection")
const currentScriptSrc = document.currentScript.src; 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") console.error("No canvas found on this page")
} }
@ -22,10 +23,38 @@
const allCandidatesCollected = new Promise(res => const allCandidatesCollected = new Promise(res =>
conn.addEventListener("icecandidate", e => e.candidate == null && 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() const canvasStreamTracks = canvasStream.getVideoTracks()
if(canvasStreamTracks.length > 0){ if(canvasStreamTracks.length > 0){
conn.addTrack(canvasStreamTracks[0], canvasStream) 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(); 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>