Compare commits
13 Commits
050f887398
...
main
Author | SHA1 | Date | |
---|---|---|---|
e854cdf234
|
|||
8af2650aca
|
|||
470147c5cd
|
|||
4d4591fb2d
|
|||
e5b57dd3d7
|
|||
d5c8fcd055
|
|||
980d6822d0
|
|||
68b20a8b4f
|
|||
c5fb07f6da
|
|||
c4f1e4d8c0
|
|||
d62722ce53
|
|||
3a3e008e90
|
|||
74e8731fa4
|
14
README.md
14
README.md
@ -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.
|
10
loled.js
10
loled.js
@ -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
37
static/camera.html
Normal 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
40
static/canvas.html
Normal 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
25
static/css/style.css
Normal 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;
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
@ -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)
|
||||||
)
|
)
|
@ -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
42
static/js/rtc.js
Normal 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
24
static/screenshare.html
Normal 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>
|
Reference in New Issue
Block a user