Compare commits

...

4 Commits

28 changed files with 691 additions and 35 deletions

View File

@ -1,2 +1,3 @@
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
ARCAST_DESKTOP_INSTANCE_ID=
ARCAST_DESKTOP_APPS=true

View File

@ -4,6 +4,7 @@ GORELEASER_ARGS ?= release --snapshot --rm-dist
GITCHLOG_ARGS ?=
SHELL := /bin/bash
JDK_PATH ?= /usr/lib/jvm/java-11-openjdk
GOARCH ?= amd64
MKT_GITEA_RELEASE_PROJECT := arcast
MKT_GITEA_RELEASE_ORG := arcad
@ -22,9 +23,9 @@ test-go: deps
build: build-desktop build-android build-client ## Build artefacts
build-desktop: deps ## Build executable
CGO_ENABLED=0 go build \
CGO_ENABLED=0 GOARCH=$(GOARCH) go build \
-v \
-o ./bin/desktop \
-o ./bin/desktop_$(GOARCH) \
./cmd/desktop
build-client: deps ## Build executable
@ -35,9 +36,14 @@ build-client: deps ## Build executable
build-android: tools/gogio/bin/gogio deps ## Build executable
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 )
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
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
@ -66,7 +72,14 @@ gitea-release: .mktools build
rm -rf .gitea-release
mkdir -p .gitea-release
cp ./bin/desktop .gitea-release/arcad_player_linux_amd64
cp ./bin/desktop_amd64 .gitea-release/arcad_player_linux_amd64
$(MAKE) GOARCH=arm build-desktop
cp ./bin/desktop_arm .gitea-release/arcad_player_linux_arm
$(MAKE) GOARCH=arm64 build-desktop
cp ./bin/desktop_arm64 .gitea-release/arcad_player_linux_arm64
cp ./bin/client .gitea-release/arcad_client_linux_amd64
cp ./android/app/build/outputs/apk/debug/app-debug.apk .gitea-release/arcast_player_debug.apk

View File

@ -32,4 +32,4 @@ Avoir [Chromium](https://www.chromium.org/chromium-projects/) (ou `Google Chrome
#### Dernière version
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_amd64)
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_amd64), [`arm`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_arm), [`arm64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_arm64)

19
apps/home/app.js Normal file
View File

@ -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);
});

16
apps/home/index.html Normal file
View File

@ -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>

10
apps/home/manifest.json Normal file
View File

@ -0,0 +1,10 @@
{
"title": {
"fr": "Accueil",
"en": "Home"
},
"description": {
"fr": "Voir la liste des applications",
"en": "See apps list"
}
}

23
apps/home/style.css Normal file
View File

@ -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;
}

3
apps/lib/manifest.json Normal file
View File

@ -0,0 +1,3 @@
{
"hidden": true
}

77
apps/lib/style.css Normal file
View File

@ -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%;
}

View File

@ -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()

View File

@ -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>

View File

@ -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"
}
}

116
apps/screen-sharing/app.js Normal file
View File

@ -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()

View File

@ -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>

View File

@ -0,0 +1,10 @@
{
"title": {
"fr": "Partage d'écran",
"en": "Screen sharing"
},
"description": {
"fr": "Partager son écran",
"en": "Share your screen"
}
}

View File

@ -8,6 +8,7 @@ import (
"os"
"sync"
"forge.cadoles.com/arcad/arcast"
"forge.cadoles.com/arcad/arcast/pkg/browser/gioui"
"forge.cadoles.com/arcad/arcast/pkg/server"
"gioui.org/app"
@ -77,7 +78,13 @@ func main() {
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 {
logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err)))

70
default_apps.go Normal file
View File

@ -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
}

10
go.mod
View File

@ -4,12 +4,13 @@ go 1.21.4
require (
gioui.org v0.4.1
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0
github.com/davecgh/go-spew v1.1.1
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
github.com/google/uuid v1.3.0
github.com/go-chi/cors v1.2.1
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
github.com/jaevor/go-nanoid v1.3.0
github.com/pkg/errors v0.9.1
github.com/wlynxg/anet v0.0.1
github.com/zserge/lorca v0.1.10
)
@ -17,6 +18,7 @@ require (
cdr.dev/slog v1.6.1 // indirect
gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 // indirect
gioui.org/shader v1.0.8 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
@ -25,7 +27,6 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a // indirect
github.com/jaevor/go-nanoid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@ -37,7 +38,6 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/wlynxg/anet v0.0.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
@ -60,3 +60,5 @@ require (
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)
replace github.com/zserge/lorca => github.com/Bornholm/lorca v0.0.0-20240121134933-d5e83569cb4c

8
go.sum
View File

@ -19,6 +19,8 @@ gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/Bornholm/lorca v0.0.0-20240121134933-d5e83569cb4c h1:xG9xSpEqZYOvd1Bvx/36i3RBTA17d7XnuvoMIG+/ry8=
github.com/Bornholm/lorca v0.0.0-20240121134933-d5e83569cb4c/go.mod h1:TUOtEogaMwy0b+p9e4NxZC1jbPRUaNaEw5DnjQF//F8=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@ -34,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/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/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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -50,8 +54,6 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 h1:cNb52t5fkWv8ZiicKWnc2eZnhsCCoH7WmRBMIbMp04Q=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1/go.mod h1:I6CSXU4zCGL08JOk9NbcT0ofAgnIkS/fVXbYzfSoDic=
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a h1:uZklbtdSPrDL/d1EUKd9s8a0Byla2TT01Wg/GZ4xj0w=
@ -104,8 +106,6 @@ github.com/wlynxg/anet v0.0.1/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zserge/lorca v0.1.10 h1:f/xBJ3D3ipcVRCcvN8XqZnpoKcOXV8I4vwqlFyw7ruc=
github.com/zserge/lorca v0.1.10/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A=
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=

View File

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"forge.cadoles.com/arcad/arcast"
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
"forge.cadoles.com/arcad/arcast/pkg/server"
"github.com/pkg/errors"
@ -26,11 +27,21 @@ func Run() *cli.Command {
EnvVars: []string{"ARCAST_DESKTOP_INSTANCE_ID"},
Value: "",
},
&cli.StringFlag{
Name: "address",
EnvVars: []string{"ARCAST_DESKTOP_ADDRESS"},
Value: ":",
},
&cli.IntFlag{
Name: "window-height",
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"},
Value: defaults.Height,
},
&cli.BoolFlag{
Name: "apps",
EnvVars: []string{"ARCAST_DESKTOP_APPS"},
Value: false,
},
&cli.IntFlag{
Name: "window-width",
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
@ -41,6 +52,8 @@ func Run() *cli.Command {
windowHeight := ctx.Int("window-height")
windowWidth := ctx.Int("window-width")
chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...)
enableApps := ctx.Bool("apps")
serverAddress := ctx.String("address")
browser := lorca.NewBrowser(
lorca.WithAdditionalChromeArgs(chromeArgs...),
@ -69,7 +82,13 @@ func Run() *cli.Command {
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 {
return errors.Wrap(err, "could not start server")

View File

@ -1,8 +1,9 @@
**/*.go
pkg/server/templates/**.gotmpl
apps/**
modd.conf
.env {
prep: make build-client
prep: make build-desktop
daemon: make run RUN_CMD="bin/desktop --debug --log-level debug run"
daemon: make run RUN_CMD="bin/desktop_amd64 --debug --log-level debug run"
}

View File

@ -18,7 +18,10 @@ type Browser struct {
func (b *Browser) Start() error {
logger.Debug(context.Background(), "starting browser", logger.F("opts", b.opts))
ui, err := lorca.New("", "", b.opts.Width, b.opts.Height, b.opts.ChromeArgs...)
ui, err := lorca.New(
lorca.WithWindowSize(b.opts.Width, b.opts.Height),
lorca.WithAdditionalCustomArgs(b.opts.ChromeArgs...),
)
if err != nil {
return errors.WithStack(err)
}

View File

@ -14,8 +14,8 @@ var DefaultChromeArgs = []string{
func NewOptions(funcs ...OptionsFunc) *Options {
opts := &Options{
Width: 800,
Height: 600,
Width: 0,
Height: 0,
ChromeArgs: DefaultChromeArgs,
}

63
pkg/server/apps.go Normal file
View File

@ -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)
}

View File

@ -9,6 +9,7 @@ import (
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
@ -34,11 +35,36 @@ func init() {
func (s *Server) startHTTPServer(ctx context.Context) error {
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.Post("/api/v1/cast", s.handleCast)
router.Delete("/api/v1/cast", s.handleReset)
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{
Addr: s.address,
Handler: router,
@ -170,6 +196,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
IPs []string
Port int
ID string
Apps bool
}
ips, err := getLANIPv4Addrs()
@ -183,6 +210,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
ID: s.instanceID,
IPs: ips,
Port: s.port,
Apps: s.appsEnabled,
}
if err := idleTemplate.Execute(w, d); err != nil {

View File

@ -17,18 +17,24 @@ func init() {
}
type Options struct {
InstanceID string
Address string
DisableServiceDiscovery bool
InstanceID string
Address string
EnableServiceDiscovery bool
EnableApps bool
DefaultApp string
Apps []App
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
InstanceID: NewRandomInstanceID(),
Address: ":",
DisableServiceDiscovery: false,
InstanceID: NewRandomInstanceID(),
Address: ":",
EnableServiceDiscovery: true,
EnableApps: false,
DefaultApp: "",
Apps: make([]App, 0),
}
for _, fn := range funcs {
@ -38,6 +44,24 @@ func NewOptions(funcs ...OptionFunc) *Options {
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 {
return func(opts *Options) {
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) {
opts.DisableServiceDiscovery = disabled
opts.EnableServiceDiscovery = enabled
}
}

View File

@ -11,10 +11,15 @@ import (
type Server struct {
browser browser.Browser
instanceID string
address string
port int
disableServiceDiscovery bool
instanceID string
address string
port int
serviceDiscoveryEnabled bool
appsEnabled bool
defaultApp string
apps []App
ctx context.Context
cancel context.CancelFunc
@ -32,7 +37,7 @@ func (s *Server) Start() error {
return errors.WithStack(err)
}
if !s.disableServiceDiscovery {
if s.serviceDiscoveryEnabled {
mdnsServerCtx, cancelMDNSServer := context.WithCancel(serverCtx)
if err := s.startMDNServer(mdnsServerCtx); err != nil {
cancelHTTPServer()
@ -71,6 +76,9 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
browser: browser,
instanceID: opts.InstanceID,
address: opts.Address,
disableServiceDiscovery: opts.DisableServiceDiscovery,
appsEnabled: opts.EnableApps,
defaultApp: opts.DefaultApp,
apps: opts.Apps,
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
}
}

View File

@ -76,13 +76,18 @@
.text-small {
font-size: 0.8em;
}
.mt {
margin-top: 1em;
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="panel">
<h1>Arcast</h1>
<h1>Arcast - Idle</h1>
<p>Instance ID:</p>
<p class="text-centered text-small">
<code>{{ .ID }}</code>
@ -94,6 +99,15 @@
<li><code>{{ . }}:{{ $port }}</code></li>
{{end}}
</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>
</body>