feat: embed optional apps in player server
This commit is contained in:
parent
acd71c84f6
commit
98705066e2
|
@ -1,2 +1,3 @@
|
||||||
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
|
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
|
||||||
ARCAST_DESKTOP_INSTANCE_ID=
|
ARCAST_DESKTOP_INSTANCE_ID=
|
||||||
|
ARCAST_DESKTOP_APPS=true
|
7
Makefile
7
Makefile
|
@ -36,9 +36,14 @@ build-client: deps ## Build executable
|
||||||
|
|
||||||
build-android: tools/gogio/bin/gogio deps ## Build executable
|
build-android: tools/gogio/bin/gogio deps ## Build executable
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
|
CGO_ENABLED=1 GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
|
||||||
( cd android && ./gradlew assembleDebug )
|
( cd android && ./gradlew assembleDebug )
|
||||||
|
|
||||||
|
release-android: tools/gogio/bin/gogio deps ## Build executable
|
||||||
|
mkdir -p dist
|
||||||
|
CGO_ENABLED=1 GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
|
||||||
|
( cd android && ./gradlew assemble )
|
||||||
|
|
||||||
install-android: build-android
|
install-android: build-android
|
||||||
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
adb shell monkey -p com.cadoles.arcast_player -c android.intent.category.LAUNCHER 1
|
adb shell monkey -p com.cadoles.arcast_player -c android.intent.category.LAUNCHER 1
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
fetch("/api/v1/apps")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
const defaultApp = res.data.defaultApp;
|
||||||
|
const apps = res.data.apps;
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "container";
|
||||||
|
apps.forEach((app) => {
|
||||||
|
if (app.id === defaultApp || app.hidden) return;
|
||||||
|
const appLink = document.createElement("a");
|
||||||
|
appLink.className = "app-link";
|
||||||
|
appLink.href = `/apps/${app.id}/`;
|
||||||
|
appLink.innerText = app.title["fr"];
|
||||||
|
container.appendChild(appLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("main").replaceWith(container);
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Arcast - Home</title>
|
||||||
|
<link rel="stylesheet" href="/apps/lib/style.css">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script type="text/javascript" src="app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main" class="container">
|
||||||
|
<p class="text-center">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"fr": "Accueil",
|
||||||
|
"en": "Home"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"fr": "Voir la liste des applications",
|
||||||
|
"en": "See apps list"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
.mt {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-link {
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
box-shadow: 1px 1px 3px #ccc;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-link:hover {
|
||||||
|
background-color: #abdbdb;
|
||||||
|
box-shadow: 1px 1px 3px #aaa;
|
||||||
|
text-shadow: 1px 1px white;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"hidden": true
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
display: block;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-shadow: 2px 2px #3333331d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel p, .panel ul {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-small {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
function main() {
|
||||||
|
refreshStatus();
|
||||||
|
setInterval(refreshStatus, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStatus() {
|
||||||
|
return fetch("/api/v1/status")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
const newStatus = document.createElement("tr")
|
||||||
|
newStatus.id = "status"
|
||||||
|
|
||||||
|
let td = document.createElement("td")
|
||||||
|
td.innerText = res.data.status
|
||||||
|
newStatus.appendChild(td)
|
||||||
|
|
||||||
|
td = document.createElement("td")
|
||||||
|
td.innerText = res.data.title
|
||||||
|
newStatus.appendChild(td)
|
||||||
|
|
||||||
|
td = document.createElement("td")
|
||||||
|
td.innerText = res.data.url
|
||||||
|
document.getElementById("url-input").placeholder = res.data.url
|
||||||
|
newStatus.appendChild(td)
|
||||||
|
|
||||||
|
document.getElementById("status").replaceWith(newStatus)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function castUrl() {
|
||||||
|
const urlInput = document.getElementById("url-input")
|
||||||
|
const url = urlInput.value
|
||||||
|
if (url === "") return Promise.resolve()
|
||||||
|
return fetch("/api/v1/cast", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"url": url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(() => refreshStatus())
|
||||||
|
.then(() => {
|
||||||
|
urlInput.value = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
const urlInput = document.getElementById("url-input")
|
||||||
|
return fetch("/api/v1/cast", {
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(() => refreshStatus())
|
||||||
|
.then(() => {
|
||||||
|
urlInput.value = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Arcast - Remote Control</title>
|
||||||
|
<link rel="stylesheet" href="/apps/lib/style.css">
|
||||||
|
<script type="text/javascript" src="app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main" class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h1>Remote control</h1>
|
||||||
|
<table class="fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>URL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr id="status" class="text-centered">
|
||||||
|
<td colspan="3">Refreshing...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input class="text-input" id="url-input" placeholder>
|
||||||
|
<button onclick="castUrl()">Cast</button>
|
||||||
|
<button onclick="reset()">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"fr": "Contrôle à distance",
|
||||||
|
"en": "Remote control"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"fr": "Contrôler l'afficheur numérique",
|
||||||
|
"en": "Control the cast player"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
const displayMediaOptions = {
|
||||||
|
video: {
|
||||||
|
displaySurface: "browser",
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
suppressLocalAudioPlayback: false,
|
||||||
|
},
|
||||||
|
preferCurrentTab: false,
|
||||||
|
selfBrowserSurface: "exclude",
|
||||||
|
systemAudio: "include",
|
||||||
|
surfaceSwitching: "include",
|
||||||
|
monitorTypeSurfaces: "include",
|
||||||
|
};
|
||||||
|
|
||||||
|
const peerConnection = new RTCPeerConnection();
|
||||||
|
const broadcast = new BroadcastChannel("screen_sharing");
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
peerConnection.onconnectionstatechange = (evt) => {
|
||||||
|
console.log("Connection state changed", evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.oniceconnectionstatechange = () => {
|
||||||
|
console.log('ICE state: ',peerConn.iceConnectionState);
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.ontrack = function (e) {
|
||||||
|
var vid = document.createElement("video");
|
||||||
|
document.body.appendChild(vid);
|
||||||
|
vid.srcObject = e.streams[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
broadcast.onmessage = (evt) => {
|
||||||
|
const message = JSON.parse(evt.data);
|
||||||
|
console.log("Received message", message)
|
||||||
|
if (message.type === "answer") {
|
||||||
|
peerConnection.setRemoteDescription(
|
||||||
|
new RTCSessionDescription(message.payload),
|
||||||
|
() => {},
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const b64Offer = urlParams.get('offer');
|
||||||
|
if (!b64Offer) return;
|
||||||
|
|
||||||
|
const offer = JSON.parse(atob(b64Offer));
|
||||||
|
peerConnection.setRemoteDescription(
|
||||||
|
new RTCSessionDescription(offer),
|
||||||
|
() => {
|
||||||
|
peerConnection.createAnswer(function (answer) {
|
||||||
|
peerConnection.setLocalDescription(
|
||||||
|
answer,
|
||||||
|
function () {
|
||||||
|
broadcast.postMessage(JSON.stringify({
|
||||||
|
type: 'answer',
|
||||||
|
payload: answer
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, error);
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareScreen() {
|
||||||
|
return navigator.mediaDevices
|
||||||
|
.getDisplayMedia(displayMediaOptions)
|
||||||
|
.then(captureStream => {
|
||||||
|
peerConnection.addStream(captureStream);
|
||||||
|
|
||||||
|
peerConnection.createOffer(function (offer) {
|
||||||
|
peerConnection.setLocalDescription(
|
||||||
|
offer,
|
||||||
|
() => {
|
||||||
|
const b64Offer = btoa(JSON.stringify(offer))
|
||||||
|
const url = "https://127.0.0.1:" + window.location.port + "?offer=" + encodeURIComponent(b64Offer)
|
||||||
|
console.log(url)
|
||||||
|
fetch("/api/v1/cast", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"url": url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, error);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function endCall() {
|
||||||
|
var videos = document.getElementsByTagName("video");
|
||||||
|
for (var i = 0; i < videos.length; i++) {
|
||||||
|
videos[i].pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(err) {
|
||||||
|
console.error(err);
|
||||||
|
endCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Arcast - Screen sharing</title>
|
||||||
|
<link rel="stylesheet" href="/apps/lib/style.css">
|
||||||
|
<script type="text/javascript" src="app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main" class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h1>Screen sharing</h1>
|
||||||
|
<button onclick="shareScreen()">Start</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"fr": "Partage d'écran",
|
||||||
|
"en": "Screen sharing"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"fr": "Partager son écran",
|
||||||
|
"en": "Share your screen"
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/arcast"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser/gioui"
|
"forge.cadoles.com/arcad/arcast/pkg/browser/gioui"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/server"
|
"forge.cadoles.com/arcad/arcast/pkg/server"
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
|
@ -77,7 +78,13 @@ func main() {
|
||||||
logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err)))
|
logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
server := server.New(browser, server.WithInstanceID(instanceID))
|
server := server.New(
|
||||||
|
browser,
|
||||||
|
server.WithInstanceID(instanceID),
|
||||||
|
server.WithAppsEnabled(true),
|
||||||
|
server.WithDefautApp("home"),
|
||||||
|
server.WithApps(arcast.DefaultApps...),
|
||||||
|
)
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err)))
|
logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package arcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/arcast/pkg/server"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultApps []server.App
|
||||||
|
//go:embed apps/**
|
||||||
|
appsFS embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultApps, err := loadApps("apps/*")
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultApps = defaultApps
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadApps(dirPattern string) ([]server.App, error) {
|
||||||
|
apps := make([]server.App, 0)
|
||||||
|
|
||||||
|
files, err := fs.Glob(appsFS, dirPattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
stat, err := fs.Stat(appsFS, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stat.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rawManifest, err := fs.ReadFile(appsFS, filepath.Join(f, "manifest.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var app server.App
|
||||||
|
|
||||||
|
if err := json.Unmarshal(rawManifest, &app); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.ID = filepath.Base(f)
|
||||||
|
|
||||||
|
fs, err := fs.Sub(appsFS, "apps/"+app.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.FS = fs
|
||||||
|
|
||||||
|
apps = append(apps, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps, nil
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -4,7 +4,9 @@ go 1.21.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gioui.org v0.4.1
|
gioui.org v0.4.1
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
|
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
|
||||||
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
|
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
|
||||||
github.com/jaevor/go-nanoid v1.3.0
|
github.com/jaevor/go-nanoid v1.3.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -36,6 +36,8 @@ github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c h1:naFDa
|
||||||
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c/go.mod h1:nBuRsi6udr2x6eorarLHtRkoRaWBICt+WzaE7zQXgYY=
|
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c/go.mod h1:nBuRsi6udr2x6eorarLHtRkoRaWBICt+WzaE7zQXgYY=
|
||||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/arcast"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
|
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/server"
|
"forge.cadoles.com/arcad/arcast/pkg/server"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -26,11 +27,21 @@ func Run() *cli.Command {
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_INSTANCE_ID"},
|
EnvVars: []string{"ARCAST_DESKTOP_INSTANCE_ID"},
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "address",
|
||||||
|
EnvVars: []string{"ARCAST_DESKTOP_ADDRESS"},
|
||||||
|
Value: ":",
|
||||||
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "window-height",
|
Name: "window-height",
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"},
|
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"},
|
||||||
Value: defaults.Height,
|
Value: defaults.Height,
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "apps",
|
||||||
|
EnvVars: []string{"ARCAST_DESKTOP_APPS"},
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "window-width",
|
Name: "window-width",
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
|
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
|
||||||
|
@ -41,6 +52,8 @@ func Run() *cli.Command {
|
||||||
windowHeight := ctx.Int("window-height")
|
windowHeight := ctx.Int("window-height")
|
||||||
windowWidth := ctx.Int("window-width")
|
windowWidth := ctx.Int("window-width")
|
||||||
chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...)
|
chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...)
|
||||||
|
enableApps := ctx.Bool("apps")
|
||||||
|
serverAddress := ctx.String("address")
|
||||||
|
|
||||||
browser := lorca.NewBrowser(
|
browser := lorca.NewBrowser(
|
||||||
lorca.WithAdditionalChromeArgs(chromeArgs...),
|
lorca.WithAdditionalChromeArgs(chromeArgs...),
|
||||||
|
@ -69,7 +82,13 @@ func Run() *cli.Command {
|
||||||
instanceID = server.NewRandomInstanceID()
|
instanceID = server.NewRandomInstanceID()
|
||||||
}
|
}
|
||||||
|
|
||||||
server := server.New(browser, server.WithInstanceID(instanceID))
|
server := server.New(browser,
|
||||||
|
server.WithInstanceID(instanceID),
|
||||||
|
server.WithAppsEnabled(enableApps),
|
||||||
|
server.WithDefaultApp("home"),
|
||||||
|
server.WithApps(arcast.DefaultApps...),
|
||||||
|
server.WithAddress(serverAddress),
|
||||||
|
)
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
return errors.Wrap(err, "could not start server")
|
return errors.Wrap(err, "could not start server")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
**/*.go
|
**/*.go
|
||||||
pkg/server/templates/**.gotmpl
|
pkg/server/templates/**.gotmpl
|
||||||
|
apps/**
|
||||||
modd.conf
|
modd.conf
|
||||||
.env {
|
.env {
|
||||||
prep: make build-client
|
prep: make build-client
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title map[string]string `json:"title"`
|
||||||
|
Description map[string]string `json:"description"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
FS fs.FS `json:"-"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDefaultApp(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/apps/"+s.defaultApp+"/", http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
DefaultApp string `json:"defaultApp"`
|
||||||
|
Apps []App `json:"apps"`
|
||||||
|
}{
|
||||||
|
DefaultApp: s.defaultApp,
|
||||||
|
Apps: s.apps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
indexedAppFilesystems map[string]fs.FS
|
||||||
|
indexAppsOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) handleAppFilesystem(w http.ResponseWriter, r *http.Request) {
|
||||||
|
indexAppsOnce.Do(func() {
|
||||||
|
indexedAppFilesystems = make(map[string]fs.FS, len(s.apps))
|
||||||
|
for _, app := range s.apps {
|
||||||
|
indexedAppFilesystems[app.ID] = app.FS
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
appID := chi.URLParam(r, "appID")
|
||||||
|
|
||||||
|
fs, exists := indexedAppFilesystems[appID]
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimPrefix(r.URL.Path, "/apps/"+appID)
|
||||||
|
|
||||||
|
http.ServeFileFS(w, r, fs, name)
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/api"
|
"gitlab.com/wpetit/goweb/api"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -34,11 +35,36 @@ func init() {
|
||||||
func (s *Server) startHTTPServer(ctx context.Context) error {
|
func (s *Server) startHTTPServer(ctx context.Context) error {
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
if s.appsEnabled {
|
||||||
|
ips, err := getLANIPv4Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedOrigins := make([]string, len(ips))
|
||||||
|
for idx, ip := range ips {
|
||||||
|
allowedOrigins[idx] = fmt.Sprintf("http://%s:%d", ip, s.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: allowedOrigins,
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||||
|
AllowCredentials: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
router.Get("/", s.handleHome)
|
router.Get("/", s.handleHome)
|
||||||
router.Post("/api/v1/cast", s.handleCast)
|
router.Post("/api/v1/cast", s.handleCast)
|
||||||
router.Delete("/api/v1/cast", s.handleReset)
|
router.Delete("/api/v1/cast", s.handleReset)
|
||||||
router.Get("/api/v1/status", s.handleStatus)
|
router.Get("/api/v1/status", s.handleStatus)
|
||||||
|
|
||||||
|
if s.appsEnabled {
|
||||||
|
router.Get("/apps", s.handleDefaultApp)
|
||||||
|
router.Get("/api/v1/apps", s.handleApps)
|
||||||
|
router.Handle("/apps/{appID}/*", http.HandlerFunc(s.handleAppFilesystem))
|
||||||
|
}
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: s.address,
|
Addr: s.address,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
@ -170,6 +196,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
IPs []string
|
IPs []string
|
||||||
Port int
|
Port int
|
||||||
ID string
|
ID string
|
||||||
|
Apps bool
|
||||||
}
|
}
|
||||||
|
|
||||||
ips, err := getLANIPv4Addrs()
|
ips, err := getLANIPv4Addrs()
|
||||||
|
@ -183,6 +210,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
ID: s.instanceID,
|
ID: s.instanceID,
|
||||||
IPs: ips,
|
IPs: ips,
|
||||||
Port: s.port,
|
Port: s.port,
|
||||||
|
Apps: s.appsEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := idleTemplate.Execute(w, d); err != nil {
|
if err := idleTemplate.Execute(w, d); err != nil {
|
||||||
|
|
|
@ -17,18 +17,24 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
InstanceID string
|
InstanceID string
|
||||||
Address string
|
Address string
|
||||||
DisableServiceDiscovery bool
|
EnableServiceDiscovery bool
|
||||||
|
EnableApps bool
|
||||||
|
DefaultApp string
|
||||||
|
Apps []App
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(opts *Options)
|
type OptionFunc func(opts *Options)
|
||||||
|
|
||||||
func NewOptions(funcs ...OptionFunc) *Options {
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
opts := &Options{
|
opts := &Options{
|
||||||
InstanceID: NewRandomInstanceID(),
|
InstanceID: NewRandomInstanceID(),
|
||||||
Address: ":",
|
Address: ":",
|
||||||
DisableServiceDiscovery: false,
|
EnableServiceDiscovery: true,
|
||||||
|
EnableApps: false,
|
||||||
|
DefaultApp: "",
|
||||||
|
Apps: make([]App, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range funcs {
|
for _, fn := range funcs {
|
||||||
|
@ -38,6 +44,24 @@ func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithAppsEnabled(enabled bool) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.EnableApps = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDefaultApp(defaultApp string) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.DefaultApp = defaultApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithApps(apps ...App) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.Apps = apps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithAddress(addr string) OptionFunc {
|
func WithAddress(addr string) OptionFunc {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Address = addr
|
opts.Address = addr
|
||||||
|
@ -50,9 +74,9 @@ func WithInstanceID(id string) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithServiceDiscoveryDisabled(disabled bool) OptionFunc {
|
func WithServiceDiscoveryEnabled(enabled bool) OptionFunc {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.DisableServiceDiscovery = disabled
|
opts.EnableServiceDiscovery = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,15 @@ import (
|
||||||
type Server struct {
|
type Server struct {
|
||||||
browser browser.Browser
|
browser browser.Browser
|
||||||
|
|
||||||
instanceID string
|
instanceID string
|
||||||
address string
|
address string
|
||||||
port int
|
port int
|
||||||
disableServiceDiscovery bool
|
|
||||||
|
serviceDiscoveryEnabled bool
|
||||||
|
|
||||||
|
appsEnabled bool
|
||||||
|
defaultApp string
|
||||||
|
apps []App
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
@ -32,7 +37,7 @@ func (s *Server) Start() error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.disableServiceDiscovery {
|
if s.serviceDiscoveryEnabled {
|
||||||
mdnsServerCtx, cancelMDNSServer := context.WithCancel(serverCtx)
|
mdnsServerCtx, cancelMDNSServer := context.WithCancel(serverCtx)
|
||||||
if err := s.startMDNServer(mdnsServerCtx); err != nil {
|
if err := s.startMDNServer(mdnsServerCtx); err != nil {
|
||||||
cancelHTTPServer()
|
cancelHTTPServer()
|
||||||
|
@ -71,6 +76,9 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
|
||||||
browser: browser,
|
browser: browser,
|
||||||
instanceID: opts.InstanceID,
|
instanceID: opts.InstanceID,
|
||||||
address: opts.Address,
|
address: opts.Address,
|
||||||
disableServiceDiscovery: opts.DisableServiceDiscovery,
|
appsEnabled: opts.EnableApps,
|
||||||
|
defaultApp: opts.DefaultApp,
|
||||||
|
apps: opts.Apps,
|
||||||
|
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,13 +76,18 @@
|
||||||
.text-small {
|
.text-small {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h1>Arcast</h1>
|
<h1>Arcast - Idle</h1>
|
||||||
<p>Instance ID:</p>
|
<p>Instance ID:</p>
|
||||||
<p class="text-centered text-small">
|
<p class="text-centered text-small">
|
||||||
<code>{{ .ID }}</code>
|
<code>{{ .ID }}</code>
|
||||||
|
@ -94,6 +99,15 @@
|
||||||
<li><code>{{ . }}:{{ $port }}</code></li>
|
<li><code>{{ . }}:{{ $port }}</code></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{if .Apps }}
|
||||||
|
<p>Apps:</p>
|
||||||
|
<ul class="text-italic text-small">
|
||||||
|
{{ $port := .Port }}
|
||||||
|
{{range .IPs}}
|
||||||
|
<li><a target="_blank" href="http://{{ . }}:{{ $port }}/apps">http://{{ . }}:{{ $port }}/apps</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in New Issue