First working version
This commit is contained in:
commit
b6fb65eb0a
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.lint": true,
|
||||||
|
"deno.unstable": false
|
||||||
|
}
|
105
loled.js
Executable file
105
loled.js
Executable file
@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/env -S deno run --allow-net --allow-env --allow-read=.
|
||||||
|
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
||||||
|
|
||||||
|
let router = new Router()
|
||||||
|
|
||||||
|
let displayIndex = 0;
|
||||||
|
let display = null;
|
||||||
|
|
||||||
|
router.get("/_loled/display/register", (ctx) => {
|
||||||
|
|
||||||
|
let name = ctx.request.url.searchParams.get("name");
|
||||||
|
if(!name){
|
||||||
|
name = "display-"+displayIndex
|
||||||
|
}
|
||||||
|
displayIndex++;
|
||||||
|
|
||||||
|
const target = ctx.sendEvents();
|
||||||
|
display = { name, sse: target }
|
||||||
|
target.dispatchMessage({ type: "welcome", name });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/_loled/grab-display",async (ctx) => {
|
||||||
|
const bdy = await ctx.request.body({type: "json"}).value;
|
||||||
|
|
||||||
|
if(display){
|
||||||
|
display.sse.dispatchMessage({ type: "offer", offer: bdy.offer, iceCandidates: bdy.iceCandidates });
|
||||||
|
|
||||||
|
const answr = await new Promise((res, rej) => {
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
rej(new Error("Timeout"))
|
||||||
|
delete display.putAnswer
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
display.putAnswer = (answr) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
delete display.putAnswer
|
||||||
|
res(answr)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.response.type = "application/json"
|
||||||
|
ctx.response.body = JSON.stringify(answr)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ctx.response.status = 404
|
||||||
|
ctx.response.body = "No display available"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post("/_loled/display/put-answer", async (ctx) => {
|
||||||
|
const bdy = await ctx.request.body({type: "json"}).value;
|
||||||
|
|
||||||
|
if(display){
|
||||||
|
if(display.putAnswer){
|
||||||
|
display.putAnswer(bdy)
|
||||||
|
ctx.response.status = 200
|
||||||
|
} else {
|
||||||
|
ctx.response.status = 404
|
||||||
|
ctx.response.body = "No offer pending"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.response.status = 404
|
||||||
|
ctx.response.body = "No display available"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new Application();
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
app.use((ctx, next) => {
|
||||||
|
ctx.response.headers.append("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||||
|
ctx.response.headers.append("Access-Control-Allow-Origin", "*")
|
||||||
|
ctx.response.headers.append("Access-Control-Max-Age", "3600")
|
||||||
|
ctx.response.headers.append("Access-Control-Allow-Headers", "*")
|
||||||
|
|
||||||
|
if(ctx.request.method == "OPTION"){
|
||||||
|
ctx.response.status = 200
|
||||||
|
} else {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
if(ctx.request.url.pathname.startsWith("/_loled"))
|
||||||
|
return next()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ctx.send({
|
||||||
|
root: `${Deno.cwd()}/static`,
|
||||||
|
index: "index.html",
|
||||||
|
path: ctx.request.url.pathname
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
ctx.response.status = 404
|
||||||
|
ctx.response.body = "Not found"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(router.routes())
|
||||||
|
app.use(router.allowedMethods())
|
||||||
|
|
||||||
|
await app.listen({ port: Deno.env.get("PORT") || 80 });
|
59
static/display/index.html
Normal file
59
static/display/index.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Display</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: monospace;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
background: black;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100vh;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display-name {
|
||||||
|
width: var(--display-height);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transform: rotate(90deg) translateY(-100%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
font-size: 1.3rem;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 15px;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="display">
|
||||||
|
<p id="display-name">LOLED<br/>http://10.0.0.28/</p>
|
||||||
|
<video id="display-video" muted></video>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="../js/display.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
static/index.html
Normal file
39
static/index.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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>
|
||||||
|
</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>
|
95
static/js/display.js
Normal file
95
static/js/display.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
async function init(
|
||||||
|
name="loled-display",
|
||||||
|
width = window.innerWidth,
|
||||||
|
height = window.innerHeight,
|
||||||
|
){
|
||||||
|
|
||||||
|
const display = document.getElementById("display")
|
||||||
|
const video = document.getElementById("display-video")
|
||||||
|
const nameEl = document.getElementById("display-name")
|
||||||
|
let peerConnection = null
|
||||||
|
|
||||||
|
async function setNewConnection(offer, iceCandidates){
|
||||||
|
const connection = new RTCPeerConnection({
|
||||||
|
iceServers: [
|
||||||
|
{urls: ["stun:stun.nextcloud.com:443"]}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
connection.addEventListener("track", (e) => {
|
||||||
|
if(e.streams.length > 0){
|
||||||
|
video.srcObject = e.streams[0]
|
||||||
|
video.play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
connection.addEventListener("connectionstatechange", () => {
|
||||||
|
if(connection.connectionState == "connected"){
|
||||||
|
nameEl.hidden = true
|
||||||
|
} else if(["closed", "failed", "disconnected"].indexOf(connection.connectionState)) {
|
||||||
|
nameEl.hidden = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
connection.setRemoteDescription(new RTCSessionDescription(offer))
|
||||||
|
for(const it of iceCandidates){
|
||||||
|
connection.addIceCandidate(it ? new RTCIceCandidate(it) : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const answr = await connection.createAnswer()
|
||||||
|
connection.setLocalDescription(answr)
|
||||||
|
await fetch("/_loled/display/put-answer", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(answr),
|
||||||
|
headers: {"content-type": "application/json"}
|
||||||
|
})
|
||||||
|
console.log("New offer received")
|
||||||
|
|
||||||
|
if(peerConnection){
|
||||||
|
peerConnection.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection = connection
|
||||||
|
nameEl.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
display.style.width = width+"px"
|
||||||
|
display.style.height = height+"px"
|
||||||
|
display.style.setProperty("--display-width", width+"px")
|
||||||
|
display.style.setProperty("--display-height", height+"px")
|
||||||
|
|
||||||
|
const [registration, registerdName] = await getRegistration(name)
|
||||||
|
name = registerdName;
|
||||||
|
|
||||||
|
nameEl.hidden = false
|
||||||
|
|
||||||
|
registration.addEventListener("message", e => {
|
||||||
|
const message = JSON.parse(e.data)
|
||||||
|
if(message.type == "offer"){
|
||||||
|
setNewConnection(message.offer, message.iceCandidates)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRegistration(name){
|
||||||
|
console.info("Waiting welcome message")
|
||||||
|
const reg = new EventSource("/_loled/display/register?name="+name)
|
||||||
|
const registeredName = await new Promise((res, rej) => {
|
||||||
|
reg.addEventListener("message", e => {
|
||||||
|
const message = JSON.parse(e.data)
|
||||||
|
if(message.type != 'welcome'){
|
||||||
|
rej(new Error("Invalid message received"))
|
||||||
|
}
|
||||||
|
res(message.name)
|
||||||
|
}, {once: true})
|
||||||
|
})
|
||||||
|
console.info(`display registerd with name ${registeredName}`)
|
||||||
|
return [reg, registeredName]
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = new URLSearchParams(location.search);
|
||||||
|
init(
|
||||||
|
args.get("name"),
|
||||||
|
parseInt(args.get("width")),
|
||||||
|
parseInt(args.get("height"))
|
||||||
|
)
|
48
static/js/grab-canvas.js
Normal file
48
static/js/grab-canvas.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
(async () => {
|
||||||
|
const globalConnnection = Symbol("grab-canvas-connection")
|
||||||
|
|
||||||
|
const currentScriptSrc = document.currentScript.src;
|
||||||
|
const canvas = document.querySelector("canvas");
|
||||||
|
|
||||||
|
if(!canvas){
|
||||||
|
console.error("No canvas found on this page")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 canvasStream = canvas.captureStream()
|
||||||
|
const canvasStreamTracks = canvasStream.getVideoTracks()
|
||||||
|
if(canvasStreamTracks.length > 0){
|
||||||
|
conn.addTrack(canvasStreamTracks[0], canvasStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
const offer = await conn.createOffer();
|
||||||
|
await conn.setLocalDescription(offer);
|
||||||
|
|
||||||
|
await allCandidatesCollected
|
||||||
|
|
||||||
|
const res = await fetch(new URL("/_loled/grab-display", currentScriptSrc), {
|
||||||
|
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
|
||||||
|
})()
|
Loading…
Reference in New Issue
Block a user