Compare commits
1 Commits
2024.4.26-
...
2024.4.23-
Author | SHA1 | Date | |
---|---|---|---|
e21964ee46 |
@ -1,4 +1,3 @@
|
|||||||
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
|
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
|
||||||
ARCAST_DESKTOP_INSTANCE_ID=
|
ARCAST_DESKTOP_INSTANCE_ID=
|
||||||
ARCAST_DESKTOP_APPS=true
|
ARCAST_DESKTOP_APPS=true
|
||||||
ARCAST_DESKTOP_ALLOWED_ORIGINS="*"
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,5 +3,4 @@
|
|||||||
/.env
|
/.env
|
||||||
/tools
|
/tools
|
||||||
.mktools/
|
.mktools/
|
||||||
/.gitea-release
|
/.gitea-release
|
||||||
*.keystore
|
|
@ -1,98 +0,0 @@
|
|||||||
project_name: arcast
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
builds:
|
|
||||||
- id: arcast-player
|
|
||||||
binary: arcast-player
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
ldflags:
|
|
||||||
- -s
|
|
||||||
- -w
|
|
||||||
- -X 'main.CommitRef={{ .Commit }}'
|
|
||||||
- -X 'main.Version={{ .Version }}'
|
|
||||||
gcflags:
|
|
||||||
- -trimpath="${PWD}"
|
|
||||||
asmflags:
|
|
||||||
- -trimpath="${PWD}"
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
main: ./cmd/desktop
|
|
||||||
- id: arcast-client
|
|
||||||
binary: arcast-client
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
ldflags:
|
|
||||||
- -s
|
|
||||||
- -w
|
|
||||||
- -X 'main.CommitRef={{ .Commit }}'
|
|
||||||
- -X 'main.Version={{ .Version }}'
|
|
||||||
gcflags:
|
|
||||||
- -trimpath="${PWD}"
|
|
||||||
asmflags:
|
|
||||||
- -trimpath="${PWD}"
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
main: ./cmd/client
|
|
||||||
archives:
|
|
||||||
- id: arcast-client
|
|
||||||
builds: ["arcast-client"]
|
|
||||||
name_template: '{{ .ProjectName }}-client_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
files:
|
|
||||||
- README.md
|
|
||||||
- id: arcast-player
|
|
||||||
builds: ["arcast-player"]
|
|
||||||
name_template: '{{ .ProjectName }}-player_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
|
||||||
files:
|
|
||||||
- README.md
|
|
||||||
checksum:
|
|
||||||
name_template: "checksums.txt"
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ .Version }}"
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
nfpms:
|
|
||||||
- id: arcast-player
|
|
||||||
builds:
|
|
||||||
- "arcast-player"
|
|
||||||
package_name: arcast-player
|
|
||||||
homepage: https://forge.cadoles.com/arcad/arcast
|
|
||||||
maintainer: Cadoles <contact@cadoles.com>
|
|
||||||
description: |-
|
|
||||||
Web diffusion player
|
|
||||||
license: AGPL-3.0
|
|
||||||
formats:
|
|
||||||
- apk
|
|
||||||
- deb
|
|
||||||
dependencies:
|
|
||||||
- chromium | chromium-browser | google-chrome-stable
|
|
||||||
contents:
|
|
||||||
- src: misc/packaging/player.desktop
|
|
||||||
dst: /usr/share/applications/arcast-player.desktop
|
|
||||||
type: config
|
|
||||||
- src: misc/logo/icon.png
|
|
||||||
dst: /usr/share/pixmaps/arcast-player.png
|
|
||||||
type: config
|
|
||||||
- id: arcast-client
|
|
||||||
builds:
|
|
||||||
- "arcast-client"
|
|
||||||
package_name: arcast-client
|
|
||||||
homepage: https://forge.cadoles.com/arcad/arcast
|
|
||||||
maintainer: Cadoles <contact@cadoles.com>
|
|
||||||
description: |-
|
|
||||||
Arcast player command-line client
|
|
||||||
license: AGPL-3.0
|
|
||||||
formats:
|
|
||||||
- apk
|
|
||||||
- deb
|
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"java.project.sourcePaths": [
|
|
||||||
"apps/main/src"
|
|
||||||
]
|
|
||||||
}
|
|
58
Jenkinsfile
vendored
58
Jenkinsfile
vendored
@ -3,63 +3,17 @@
|
|||||||
// Utilisation du pipeline "standard"
|
// Utilisation du pipeline "standard"
|
||||||
// Voir https://forge.cadoles.com/Cadoles/Jenkins/src/branch/master/doc/tutorials/standard-make-pipeline.md
|
// Voir https://forge.cadoles.com/Cadoles/Jenkins/src/branch/master/doc/tutorials/standard-make-pipeline.md
|
||||||
standardMakePipeline([
|
standardMakePipeline([
|
||||||
'baseImage': 'reg.cadoles.com/proxy_cache/library/ubuntu:24.04',
|
|
||||||
'dockerfileExtension': '''
|
'dockerfileExtension': '''
|
||||||
ARG GOLANG_VERSION=1.22.0
|
RUN apt-get update \
|
||||||
ARG NODEJS_VERSION=20.x
|
&& apt-get install -y zip jq
|
||||||
|
|
||||||
ENV ANDROID_HOME=/opt/android-sdk-linux
|
RUN wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz \
|
||||||
ENV ANDROID_SDK_ROOT=${ANDROID_HOME}
|
&& rm -rf /usr/local/go \
|
||||||
ENV ANDROID_NDK_HOME=${ANDROID_HOME}/ndk-bundle
|
&& tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
|
||||||
ENV PATH=${PATH}:/usr/local/go/bin/:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools
|
|
||||||
|
|
||||||
# Install utilities
|
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||||
RUN apt-get update -y \
|
|
||||||
&& apt-get install -y wget curl tar build-essential ca-certificates git openjdk-17-jdk unzip locales jq moreutils
|
|
||||||
|
|
||||||
# Set locale
|
|
||||||
RUN locale-gen en_US.UTF-8
|
|
||||||
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'
|
|
||||||
|
|
||||||
# Install NodeJS
|
|
||||||
RUN wget -O- https://deb.nodesource.com/setup_${NODEJS_VERSION} | bash - \
|
|
||||||
&& apt-get update -y \
|
|
||||||
&& apt-get install -y nodejs
|
|
||||||
|
|
||||||
# Install Golang
|
|
||||||
RUN wget -O golang.tar.gz https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \
|
|
||||||
&& tar -C /usr/local -xzf golang.tar.gz
|
|
||||||
|
|
||||||
# Install Android SDK/NDK
|
|
||||||
RUN mkdir -p /opt/android-sdk-linux
|
|
||||||
WORKDIR /opt/android-sdk-linux
|
|
||||||
|
|
||||||
RUN wget -O commandlinetools.zip https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip \
|
|
||||||
&& mkdir -p ${ANDROID_HOME}/cmdline-tools/latest \
|
|
||||||
&& unzip -q commandlinetools.zip -d ${ANDROID_HOME}/cmdline-tools/latest \
|
|
||||||
&& mv ${ANDROID_HOME}/cmdline-tools/latest/cmdline-tools/* ${ANDROID_HOME}/cmdline-tools/latest/ \
|
|
||||||
&& rm -f commandlinetools.zip
|
|
||||||
|
|
||||||
RUN echo y | cmdline-tools/latest/bin/sdkmanager --install \
|
|
||||||
'platforms;android-31' \
|
|
||||||
'ndk-bundle' \
|
|
||||||
'build-tools;34.0.0' \
|
|
||||||
'tools' \
|
|
||||||
'platform-tools' \
|
|
||||||
'extras;android;m2repository' \
|
|
||||||
'extras;google;m2repository' \
|
|
||||||
'extras;google;google_play_services'
|
|
||||||
''',
|
''',
|
||||||
'hooks': [
|
|
||||||
'pre-release': {
|
|
||||||
sh '''
|
|
||||||
make .mktools
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'credentials': [
|
'credentials': [
|
||||||
file(credentialsId: 'android-keystore', variable: 'ANDROID_KEYSTORE_FILE'),
|
|
||||||
string(credentialsId: 'android-keystore-pass', variable: 'ANDROID_KEYSTORE_PASS'),
|
|
||||||
usernamePassword(credentialsId: 'kipp-credentials', usernameVariable: 'MKT_GITEA_RELEASE_USERNAME', passwordVariable: 'MKT_GITEA_RELEASE_PASSWORD')
|
usernamePassword(credentialsId: 'kipp-credentials', usernameVariable: 'MKT_GITEA_RELEASE_USERNAME', passwordVariable: 'MKT_GITEA_RELEASE_PASSWORD')
|
||||||
]
|
]
|
||||||
])
|
])
|
72
Makefile
72
Makefile
@ -1,6 +1,6 @@
|
|||||||
LINT_ARGS ?= --timeout 5m
|
LINT_ARGS ?= --timeout 5m
|
||||||
GORELEASER_VERSION ?= v1.25.1
|
GORELEASER_VERSION ?= v1.13.1
|
||||||
GORELEASER_ARGS ?= release --snapshot --clean
|
GORELEASER_ARGS ?= release --snapshot --rm-dist
|
||||||
GITCHLOG_ARGS ?=
|
GITCHLOG_ARGS ?=
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
JDK_PATH ?= /usr/lib/jvm/java-11-openjdk
|
JDK_PATH ?= /usr/lib/jvm/java-11-openjdk
|
||||||
@ -11,15 +11,6 @@ MKT_GITEA_RELEASE_ORG := arcad
|
|||||||
|
|
||||||
GOTEST_ARGS ?= -short
|
GOTEST_ARGS ?= -short
|
||||||
|
|
||||||
ANDROID_KEYSTORE_FILE ?= .keystore
|
|
||||||
ANDROID_KEYSTORE_KEY_ALIAS ?= arcast
|
|
||||||
export ANDROID_KEYSTORE_PASS ?= NotSoSecret
|
|
||||||
ANDROID_KEYSTORE_KEY_VALIDITY ?= 365000
|
|
||||||
|
|
||||||
ANDROID_BUILD_TOOLS_VERSION ?= 34.0.0
|
|
||||||
|
|
||||||
APPS := main
|
|
||||||
|
|
||||||
watch: tools/modd/bin/modd deps ## Watching updated files - live reload
|
watch: tools/modd/bin/modd deps ## Watching updated files - live reload
|
||||||
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
|
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
|
||||||
|
|
||||||
@ -29,12 +20,7 @@ test: test-go ## Executing tests
|
|||||||
test-go: deps
|
test-go: deps
|
||||||
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
|
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
|
||||||
|
|
||||||
build: build-apps build-desktop build-android build-client ## Build artefacts
|
build: build-desktop build-android build-client ## Build artefacts
|
||||||
|
|
||||||
build-apps: $(foreach name,$(APPS),build-app-$(name))
|
|
||||||
|
|
||||||
build-app-%:
|
|
||||||
$(MAKE) -C apps/$* build
|
|
||||||
|
|
||||||
build-desktop: deps ## Build executable
|
build-desktop: deps ## Build executable
|
||||||
CGO_ENABLED=0 GOARCH=$(GOARCH) go build \
|
CGO_ENABLED=0 GOARCH=$(GOARCH) go build \
|
||||||
@ -51,38 +37,15 @@ 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
|
||||||
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
|
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 && VERSION_NAME=$(MKT_PROJECT_VERSION)-debug ./gradlew assembleDebug )
|
( cd android && ./gradlew assembleDebug )
|
||||||
|
|
||||||
$(ANDROID_KEYSTORE_FILE):
|
release-android: tools/gogio/bin/gogio deps ## Build executable
|
||||||
keytool -genkey -noprompt \
|
|
||||||
-alias $(ANDROID_KEYSTORE_KEY_ALIAS) \
|
|
||||||
-dname "CN=cadoles.com, OU=Dev, O=Cadoles, L=Dijon, S=Bourgogne-FrancheComté, C=FR" \
|
|
||||||
-keystore "$(ANDROID_KEYSTORE_FILE)" \
|
|
||||||
-keyalg RSA \
|
|
||||||
-validity $(ANDROID_KEYSTORE_KEY_VALIDITY) \
|
|
||||||
-storepass "$(ANDROID_KEYSTORE_PASS)"
|
|
||||||
|
|
||||||
release-android: $(ANDROID_KEYSTORE_FILE) tools/gogio/bin/gogio deps ## Build executable
|
|
||||||
mkdir -p dist
|
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
|
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 && VERSION_NAME=$(MKT_PROJECT_VERSION) ./gradlew assemble )
|
( cd android && ./gradlew assemble )
|
||||||
rm -f android/app/build/outputs/apk/release/app-release-unsigned-aligned.apk
|
|
||||||
"$(ANDROID_HOME)/build-tools/$(ANDROID_BUILD_TOOLS_VERSION)/zipalign" -p 4 android/app/build/outputs/apk/release/app-release-unsigned.apk android/app/build/outputs/apk/release/app-release-unsigned-aligned.apk
|
|
||||||
"$(ANDROID_HOME)/build-tools/$(ANDROID_BUILD_TOOLS_VERSION)/apksigner" \
|
|
||||||
sign \
|
|
||||||
--ks-key-alias $(ANDROID_KEYSTORE_KEY_ALIAS) \
|
|
||||||
--ks "$(ANDROID_KEYSTORE_FILE)" \
|
|
||||||
--ks-pass env:ANDROID_KEYSTORE_PASS \
|
|
||||||
--in android/app/build/outputs/apk/release/app-release-unsigned-aligned.apk \
|
|
||||||
--out android/app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
install-android: build-apps build-android
|
install-android: build-android
|
||||||
adb uninstall com.cadoles.arcast_player
|
|
||||||
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
$(MAKE) run-android
|
|
||||||
|
|
||||||
run-android:
|
|
||||||
adb shell am force-stop com.cadoles.arcast_player
|
|
||||||
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
|
||||||
|
|
||||||
debug-android:
|
debug-android:
|
||||||
@ -105,16 +68,20 @@ tools/gogio/bin/gogio:
|
|||||||
mkdir -p tools/gogio/bin
|
mkdir -p tools/gogio/bin
|
||||||
GOBIN=$(PWD)/tools/gogio/bin go install gioui.org/cmd/gogio@latest
|
GOBIN=$(PWD)/tools/gogio/bin go install gioui.org/cmd/gogio@latest
|
||||||
|
|
||||||
release: gitea-release
|
gitea-release: .mktools build
|
||||||
|
|
||||||
gitea-release: .mktools goreleaser release-android
|
|
||||||
rm -rf .gitea-release
|
rm -rf .gitea-release
|
||||||
mkdir -p .gitea-release
|
mkdir -p .gitea-release
|
||||||
|
|
||||||
cp ./dist/*.apk .gitea-release/
|
cp ./bin/desktop_amd64 .gitea-release/arcad_player_linux_amd64
|
||||||
cp ./dist/*.deb .gitea-release/
|
|
||||||
cp ./dist/*.tar.gz .gitea-release/
|
$(MAKE) GOARCH=arm build-desktop
|
||||||
cp ./android/app/build/outputs/apk/release/app-release.apk .gitea-release/arcast_player_$(MKT_PROJECT_VERSION).apk
|
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
|
||||||
|
|
||||||
MKT_GITEA_RELEASE_PROJECT="arcast" \
|
MKT_GITEA_RELEASE_PROJECT="arcast" \
|
||||||
MKT_GITEA_RELEASE_ORG="arcad" \
|
MKT_GITEA_RELEASE_ORG="arcad" \
|
||||||
@ -128,9 +95,6 @@ gitea-release: .mktools goreleaser release-android
|
|||||||
MKT_GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
MKT_GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||||
$(MAKE) mkt-gitea-release
|
$(MAKE) mkt-gitea-release
|
||||||
|
|
||||||
.PHONY: goreleaser
|
|
||||||
goreleaser: .env .mktools
|
|
||||||
( set -o allexport && source .env && set +o allexport && curl -sfL https://goreleaser.com/static/run | VERSION=$(GORELEASER_VERSION) GORELEASER_CURRENT_TAG="$(MKT_PROJECT_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
|
|
||||||
|
|
||||||
.PHONY: mktools
|
.PHONY: mktools
|
||||||
mktools:
|
mktools:
|
||||||
|
14
README.md
14
README.md
@ -1,7 +1,3 @@
|
|||||||
<p align="center">
|
|
||||||
<img src="./misc/logo/icon.png" width="200px" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Arcast
|
# Arcast
|
||||||
|
|
||||||
Serveur de diffusion de contenu (Web) pour afficheurs numériques (Linux et Android).
|
Serveur de diffusion de contenu (Web) pour afficheurs numériques (Linux et Android).
|
||||||
@ -16,7 +12,7 @@ Le client en ligne de commande vous permet de contrôler votre flotte de serveur
|
|||||||
|
|
||||||
#### Dernière version
|
#### Dernière version
|
||||||
|
|
||||||
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_client_linux_amd64)
|
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_client_linux_amd64)
|
||||||
|
|
||||||
### Application Android
|
### Application Android
|
||||||
|
|
||||||
@ -24,7 +20,7 @@ L'application `arcast_player` pour Android permet de transformer votre appareil
|
|||||||
|
|
||||||
#### Dernière version
|
#### Dernière version
|
||||||
|
|
||||||
- [`APK`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player.apk)
|
- [`debug`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_debug.apk)
|
||||||
|
|
||||||
### Application de bureau
|
### Application de bureau
|
||||||
|
|
||||||
@ -36,8 +32,4 @@ Avoir [Chromium](https://www.chromium.org/chromium-projects/) (ou `Google Chrome
|
|||||||
|
|
||||||
#### Dernière version
|
#### Dernière version
|
||||||
|
|
||||||
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_amd64), [`arm`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_arm), [`arm64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_arm64)
|
- 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)
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Voir le répertoire [`doc`](./doc)
|
|
@ -3,16 +3,6 @@ plugins {
|
|||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
}
|
}
|
||||||
|
|
||||||
def gitVersion() {
|
|
||||||
def counter = 0
|
|
||||||
def process = "git rev-list master --first-parent --count".execute()
|
|
||||||
return process.text.toInteger()
|
|
||||||
}
|
|
||||||
|
|
||||||
def versionName() {
|
|
||||||
return System.env.VERSION_NAME ? System.env.VERSION_NAME : "0.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.cadoles.arcast_player'
|
namespace 'com.cadoles.arcast_player'
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
@ -21,8 +11,8 @@ android {
|
|||||||
applicationId "com.cadoles.arcast_player"
|
applicationId "com.cadoles.arcast_player"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode gitVersion()
|
versionCode 1
|
||||||
versionName versionName()
|
versionName "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -57,6 +47,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||||
implementation 'androidx.activity:activity-compose:1.7.0'
|
implementation 'androidx.activity:activity-compose:1.7.0'
|
||||||
|
19
apps/home/app.js
Normal file
19
apps/home/app.js
Normal 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
16
apps/home/index.html
Normal 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>
|
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"id": "main",
|
|
||||||
"title": {
|
"title": {
|
||||||
"fr": "Accueil",
|
"fr": "Accueil",
|
||||||
"en": "Home"
|
"en": "Home"
|
23
apps/home/style.css
Normal file
23
apps/home/style.css
Normal 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;
|
||||||
|
}
|
46
apps/lib/arcast.js
Normal file
46
apps/lib/arcast.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
(function (Arcast) {
|
||||||
|
Arcast.getInfo = function () {
|
||||||
|
return fetch("/api/v1/info")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
Arcast.getBroadcastingChannel = function (id, onmessage) {
|
||||||
|
var scheme = "wss";
|
||||||
|
if (window.location.protocol === "http:") {
|
||||||
|
scheme = "ws";
|
||||||
|
}
|
||||||
|
var ws = new WebSocket(
|
||||||
|
`${scheme}://${window.location.host}/api/v1/broadcast/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
var channel = {
|
||||||
|
opened: false,
|
||||||
|
send: (message) => {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
ws.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (evt) => {
|
||||||
|
if (typeof onmessage !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(evt.data);
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
ws.onmessage = null;
|
||||||
|
ws.onclose = null;
|
||||||
|
ws.onopen = null;
|
||||||
|
channel.opened = false;
|
||||||
|
};
|
||||||
|
ws.onopen = () => {
|
||||||
|
channel.opened = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
};
|
||||||
|
})((window.Arcast = window.Arcast || {}));
|
3
apps/lib/manifest.json
Normal file
3
apps/lib/manifest.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"hidden": true
|
||||||
|
}
|
77
apps/lib/style.css
Normal file
77
apps/lib/style.css
Normal 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%;
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/exhaustive-deps": "off"
|
|
||||||
}
|
|
||||||
}
|
|
23
apps/main/.gitignore
vendored
23
apps/main/.gitignore
vendored
@ -1,23 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
@ -1,12 +0,0 @@
|
|||||||
.PHONY: build
|
|
||||||
build: node_modules
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
node_modules:
|
|
||||||
npm ci
|
|
||||||
|
|
||||||
.env.development.local:
|
|
||||||
echo "REACT_APP_ARCAST_SERVER_BASE_URL=http://127.0.0.1:45555" > .env.development.local
|
|
||||||
|
|
||||||
dev: .env.development.local
|
|
||||||
npm run start
|
|
18284
apps/main/package-lock.json
generated
18284
apps/main/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@tanstack/react-query": "^5.32.0",
|
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"@types/jest": "^27.5.2",
|
|
||||||
"@types/node": "^16.18.96",
|
|
||||||
"@types/react": "^18.2.79",
|
|
||||||
"@types/react-dom": "^18.2.25",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-router-dom": "^6.23.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"web-vitals": "^2.1.4",
|
|
||||||
"webrtc-adapter": "^9.0.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"homepage": "/apps/main",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Main | Apps | Arcast</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
@ -1,36 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
|
|
||||||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
|
||||||
import { Layout } from "./components/Layout/Layout";
|
|
||||||
|
|
||||||
import { HomePage } from "./pages/HomePage/HomePage";
|
|
||||||
import { ScreenSharingPage } from "./pages/ScreenSharingPage/ScreenSharingPage";
|
|
||||||
|
|
||||||
const router = createHashRouter([
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <Layout />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
element: <HomePage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/screen-sharing",
|
|
||||||
element: <ScreenSharingPage />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "sessions/:sessionId",
|
|
||||||
element: <ScreenSharingPage />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const App: FunctionComponent = () => {
|
|
||||||
return <RouterProvider router={router} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
@ -1,164 +0,0 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
const BASE_URL = process.env.REACT_APP_ARCAST_SERVER_BASE_URL ?? ""
|
|
||||||
|
|
||||||
export interface AppInfos {
|
|
||||||
defaultApp: string;
|
|
||||||
apps: App[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface App {
|
|
||||||
id: string;
|
|
||||||
title: { [lang: string]: string };
|
|
||||||
description: { [lang: string]: string };
|
|
||||||
icon: string;
|
|
||||||
hidden: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePlayerAppInfos = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["appsInfos"],
|
|
||||||
queryFn: getAppInfos,
|
|
||||||
select: (appInfos) => appInfos ?? { apps: [], defaultApp: "main" }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAppInfos() {
|
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/apps`);
|
|
||||||
const result = await response.json();
|
|
||||||
const appInfos = result.data as AppInfos;
|
|
||||||
return appInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface Status {
|
|
||||||
id: string
|
|
||||||
status: string
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePlayerStatus = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["playerStatus"],
|
|
||||||
queryFn: getStatus,
|
|
||||||
select: (status) => status ?? {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStatus() {
|
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/status`);
|
|
||||||
const result = await response.json();
|
|
||||||
const appInfos = result.data as Status;
|
|
||||||
return appInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BroadcastChannel {
|
|
||||||
opened: boolean
|
|
||||||
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void
|
|
||||||
close: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBroadcastChannel(channelId: string, onmessage?: (data: any) => void): BroadcastChannel {
|
|
||||||
let location: Location | URL = window.location
|
|
||||||
if (BASE_URL !== "") {
|
|
||||||
location = new URL(BASE_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
let scheme = "wss";
|
|
||||||
if (location.protocol === "http:") {
|
|
||||||
scheme = "ws";
|
|
||||||
}
|
|
||||||
|
|
||||||
var ws = new WebSocket(
|
|
||||||
`${scheme}://${location.host}/api/v1/broadcast/${channelId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const pending: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = []
|
|
||||||
|
|
||||||
var channel = {
|
|
||||||
opened: false,
|
|
||||||
send: (message: string | ArrayBufferLike | Blob | ArrayBufferView) => {
|
|
||||||
if (channel.opened) {
|
|
||||||
ws.send(message);
|
|
||||||
} else {
|
|
||||||
pending.push(message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close: () => {
|
|
||||||
ws.close();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (evt: MessageEvent<any>) => {
|
|
||||||
if (typeof onmessage !== "function") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onmessage(evt.data);
|
|
||||||
};
|
|
||||||
ws.onclose = () => {
|
|
||||||
ws.onmessage = null;
|
|
||||||
ws.onclose = null;
|
|
||||||
ws.onopen = null;
|
|
||||||
channel.opened = false;
|
|
||||||
};
|
|
||||||
ws.onopen = () => {
|
|
||||||
channel.opened = true;
|
|
||||||
while (pending.length > 0) {
|
|
||||||
const message = pending.shift()
|
|
||||||
if (message) ws.send(message)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function cast(url: string): Promise<Status> {
|
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/cast`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ url })
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
const status = result.data as Status;
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCast() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (params: { url: string }) => cast(params.url),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['playerStatus'] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function reset(): Promise<Status> {
|
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/cast`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
const status = result.data as Status;
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useReset() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: () => reset(),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['playerStatus'] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,275 +0,0 @@
|
|||||||
import { BroadcastChannel, cast, getBroadcastChannel } from "./arcast"
|
|
||||||
|
|
||||||
export class ScreenSharing extends EventTarget {
|
|
||||||
signaling: BroadcastChannel
|
|
||||||
handlers: { [messageType: string]: (data: { type: string, data: any }) => void }
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.handlers = {}
|
|
||||||
this.signaling = getBroadcastChannel("arcast.screen-sharing", this.onSignal.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
onSignal(data: any) {
|
|
||||||
let message: any
|
|
||||||
try {
|
|
||||||
message = JSON.parse(data)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof message.type !== "string") return
|
|
||||||
|
|
||||||
const handler = this.handlers[message.type]
|
|
||||||
|
|
||||||
if (!handler) return
|
|
||||||
|
|
||||||
handler(message.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
send(type: string, data: any) {
|
|
||||||
this.signaling.send(JSON.stringify({ type, data }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class ScreenSharingClient extends ScreenSharing {
|
|
||||||
sessionId: string
|
|
||||||
clientId: string
|
|
||||||
|
|
||||||
peerConnection: RTCPeerConnection | undefined
|
|
||||||
|
|
||||||
constructor(sessionId: string) {
|
|
||||||
super()
|
|
||||||
this.sessionId = sessionId
|
|
||||||
this.clientId = window.crypto.randomUUID()
|
|
||||||
this.handlers['server-offer'] = this.onServerOffer.bind(this)
|
|
||||||
this.handlers['ice-candidate'] = this.onIceCandidate.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.send("client-request", { clientId: this.clientId, sessionId: this.sessionId })
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.signaling.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
async onServerOffer(message: any) {
|
|
||||||
if (!this.checkMessage(message)) return
|
|
||||||
|
|
||||||
const conn = new RTCPeerConnection()
|
|
||||||
|
|
||||||
conn.onicecandidate = this.sendCandidate.bind(this)
|
|
||||||
conn.ontrack = this.onTrack.bind(this)
|
|
||||||
conn.onconnectionstatechange = (evt: Event) => {
|
|
||||||
const state = conn.connectionState
|
|
||||||
this.dispatchEvent(new StateChangedEvent(state))
|
|
||||||
}
|
|
||||||
|
|
||||||
await conn.setRemoteDescription(new RTCSessionDescription(message.offer))
|
|
||||||
|
|
||||||
const answer = await conn.createAnswer()
|
|
||||||
await conn.setLocalDescription(answer)
|
|
||||||
|
|
||||||
this.peerConnection = conn
|
|
||||||
|
|
||||||
this.send("client-answer", { clientId: this.clientId, sessionId: this.sessionId, answer })
|
|
||||||
}
|
|
||||||
|
|
||||||
sendCandidate(evt: RTCPeerConnectionIceEvent) {
|
|
||||||
this.send("ice-candidate", {
|
|
||||||
clientId: this.clientId,
|
|
||||||
sessionId: this.sessionId,
|
|
||||||
candidate: evt.candidate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onTrack(evt: RTCTrackEvent) {
|
|
||||||
if (!evt.streams || evt.streams.length < 1) return;
|
|
||||||
const stream = evt.streams[0]
|
|
||||||
this.dispatchEvent(new StreamChangedEvent(stream))
|
|
||||||
}
|
|
||||||
|
|
||||||
async onIceCandidate(message: any) {
|
|
||||||
if (!this.checkMessage(message)) return
|
|
||||||
if (!this.peerConnection) return
|
|
||||||
if (!message.candidate) return
|
|
||||||
|
|
||||||
await this.peerConnection.addIceCandidate(message.candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMessage(message: any) {
|
|
||||||
const sessionId = message.sessionId;
|
|
||||||
if (!this.sessionId || sessionId !== this.sessionId) return false;
|
|
||||||
|
|
||||||
if (this.clientId !== message.clientId) return false;
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ScreenSharingServerState {
|
|
||||||
Idle = "idle",
|
|
||||||
Sharing = "sharing"
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ScreenSharingServer extends ScreenSharing {
|
|
||||||
stream: MediaStream | undefined
|
|
||||||
sessionId: string | undefined
|
|
||||||
peerConnections: {
|
|
||||||
[key: string]: RTCPeerConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
state: ScreenSharingServerState
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.peerConnections = {}
|
|
||||||
this.state = ScreenSharingServerState.Idle
|
|
||||||
this.handlers['client-request'] = this.onClientRequest.bind(this)
|
|
||||||
this.handlers['client-answer'] = this.onClientAnswer.bind(this)
|
|
||||||
this.handlers['ice-candidate'] = this.onIceCandidate.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
async shareScreen(options?: DisplayMediaStreamOptions) {
|
|
||||||
try {
|
|
||||||
this.stream = await navigator.mediaDevices.getDisplayMedia(options)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessionId = window.crypto.randomUUID()
|
|
||||||
|
|
||||||
const url = `http://127.0.0.1:45555/apps/main/#screen-sharing/sessions/${this.sessionId}`
|
|
||||||
cast(url)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
window.open(`http://localhost:3000/apps/main/#screen-sharing/sessions/${this.sessionId}`, "_blank")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = ScreenSharingServerState.Sharing
|
|
||||||
this.dispatchEvent(new StateChangedEvent(ScreenSharingServerState.Sharing))
|
|
||||||
}
|
|
||||||
|
|
||||||
onClientRequest(message: any) {
|
|
||||||
if (!this.checkMessage(message)) return
|
|
||||||
this.addNewClient(message.clientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async onIceCandidate(message: any) {
|
|
||||||
if (!this.checkMessage(message)) return
|
|
||||||
|
|
||||||
const conn = this.peerConnections[message.clientId]
|
|
||||||
if (!conn) return
|
|
||||||
|
|
||||||
if (!message.candidate) return
|
|
||||||
|
|
||||||
await conn.addIceCandidate(message.candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClientAnswer(message: any) {
|
|
||||||
if (!this.checkMessage(message)) return
|
|
||||||
|
|
||||||
const conn = this.peerConnections[message.clientId]
|
|
||||||
if (!conn) return
|
|
||||||
|
|
||||||
await conn.setRemoteDescription(new RTCSessionDescription(message.answer));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMessage(message: any) {
|
|
||||||
const sessionId = message.sessionId;
|
|
||||||
if (!this.sessionId || sessionId !== this.sessionId) return false;
|
|
||||||
|
|
||||||
const clientId = message.clientId;
|
|
||||||
if (!clientId) return false;
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async addNewClient(clientId: string) {
|
|
||||||
if (!this.stream) return
|
|
||||||
|
|
||||||
const conn = new RTCPeerConnection();
|
|
||||||
const tracks = this.stream?.getVideoTracks()
|
|
||||||
|
|
||||||
if (!tracks || tracks.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.addTrack(tracks[0], this.stream)
|
|
||||||
|
|
||||||
const offer = await conn.createOffer()
|
|
||||||
await conn.setLocalDescription(offer);
|
|
||||||
|
|
||||||
conn.onicecandidate = this.sendCandidate.bind(this, clientId)
|
|
||||||
conn.onconnectionstatechange = (evt: Event) => {
|
|
||||||
const isClosed = conn.connectionState === "disconnected" ||
|
|
||||||
conn.connectionState === "failed"
|
|
||||||
if (!isClosed) return;
|
|
||||||
conn.onicecandidate = null;
|
|
||||||
conn.onconnectionstatechange = null;
|
|
||||||
conn.close();
|
|
||||||
|
|
||||||
console.log("Removing client", clientId);
|
|
||||||
delete this.peerConnections[clientId];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Adding client", clientId);
|
|
||||||
this.peerConnections[clientId] = conn;
|
|
||||||
|
|
||||||
this.send("server-offer", { sessionId: this.sessionId, clientId, offer })
|
|
||||||
}
|
|
||||||
|
|
||||||
sendCandidate(clientId: string, evt: RTCPeerConnectionIceEvent) {
|
|
||||||
this.send("ice-candidate", {
|
|
||||||
clientId,
|
|
||||||
sessionId: this.sessionId,
|
|
||||||
candidate: evt.candidate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.signaling.close()
|
|
||||||
|
|
||||||
Object.keys(this.peerConnections).forEach(clientId => {
|
|
||||||
const conn = this.peerConnections[clientId];
|
|
||||||
conn.onicecandidate = null;
|
|
||||||
conn.onconnectionstatechange = null;
|
|
||||||
conn.close();
|
|
||||||
delete this.peerConnections[clientId]
|
|
||||||
})
|
|
||||||
|
|
||||||
const tracks = this.stream?.getTracks()
|
|
||||||
if (!tracks) return
|
|
||||||
|
|
||||||
tracks.forEach(track => track.stop())
|
|
||||||
|
|
||||||
this.state = ScreenSharingServerState.Idle
|
|
||||||
this.dispatchEvent(new StateChangedEvent(ScreenSharingServerState.Idle))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StreamChangedEventType = "stream-changed"
|
|
||||||
|
|
||||||
export class StreamChangedEvent extends Event {
|
|
||||||
stream: MediaStream
|
|
||||||
|
|
||||||
constructor(stream: MediaStream) {
|
|
||||||
super(StreamChangedEventType)
|
|
||||||
this.stream = stream
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StateChangedEventType = "state-changed"
|
|
||||||
|
|
||||||
export class StateChangedEvent extends Event {
|
|
||||||
state: string
|
|
||||||
|
|
||||||
constructor(state: string) {
|
|
||||||
super(StateChangedEventType)
|
|
||||||
this.state = state
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
.root {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: hsl(208, 46%, 52%);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: hsl(208, 46%, 15%);;
|
|
||||||
font-weight: bolder;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root:hover {
|
|
||||||
background-color: hsl(208, 60%, 52%);
|
|
||||||
color: hsl(208, 60%, 15%);;;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root > a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { FunctionComponent, PropsWithChildren } from "react";
|
|
||||||
import styles from "./Button.module.css";
|
|
||||||
export interface ButtonProps extends PropsWithChildren {
|
|
||||||
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button: FunctionComponent<ButtonProps> = ({
|
|
||||||
children,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.root} onClick={onClick}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
.root {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
width: 90%;
|
|
||||||
height: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { FunctionComponent, PropsWithChildren } from "react";
|
|
||||||
import styles from "./Layout.module.css";
|
|
||||||
import { Outlet } from "react-router-dom";
|
|
||||||
|
|
||||||
export interface LayoutProps extends PropsWithChildren {}
|
|
||||||
|
|
||||||
export const Layout: FunctionComponent<LayoutProps> = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
.root {
|
|
||||||
display: block;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 10px 10px 10px #33333361;
|
|
||||||
position: relative;
|
|
||||||
padding: 30px 30px 30px 30px;
|
|
||||||
min-width: 33%;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { FunctionComponent, PropsWithChildren } from "react";
|
|
||||||
import styles from "./Panel.module.css";
|
|
||||||
export interface PanelProps extends PropsWithChildren {}
|
|
||||||
|
|
||||||
export const Panel: FunctionComponent<PanelProps> = ({ children }) => {
|
|
||||||
return <div className={styles.root}>{children}</div>;
|
|
||||||
};
|
|
@ -1,3 +0,0 @@
|
|||||||
.root {
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { FunctionComponent, PropsWithChildren } from "react";
|
|
||||||
import styles from "./Tabs.module.css";
|
|
||||||
export interface TabsProps extends PropsWithChildren {}
|
|
||||||
|
|
||||||
export const Tabs: FunctionComponent<TabsProps> = ({ children }) => {
|
|
||||||
return <div className={styles.root}>Tabs</div>;
|
|
||||||
};
|
|
@ -1,105 +0,0 @@
|
|||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
||||||
"Helvetica Neue", sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgb(76, 96, 188);
|
|
||||||
background: linear-gradient(
|
|
||||||
415deg,
|
|
||||||
rgba(4, 168, 243, 1),
|
|
||||||
rgb(76, 136, 188, 1),
|
|
||||||
rgba(76, 96, 188, 1),
|
|
||||||
rgb(115, 76, 188, 1),
|
|
||||||
rgb(87, 76, 188, 1)
|
|
||||||
);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradient 15s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*: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%;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import "./index.css";
|
|
||||||
import App from "./App";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById("root") as HTMLElement
|
|
||||||
);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<App />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
@ -1,77 +0,0 @@
|
|||||||
.root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBlock {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.columns {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns > * {
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
margin: 20px 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.castField {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 40px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.castField > input {
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
border-right: 0;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
border-left: 1px solid #ccc;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
font-size: 100%;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.castField > button {
|
|
||||||
flex-shrink: 1;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
border-right: 0;
|
|
||||||
border-left: 1px solid #ccc;
|
|
||||||
border-radius: 0;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.castField > button:hover {
|
|
||||||
background-color: #96b4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.castField > button:last-child {
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
import { ChangeEvent, FunctionComponent, useCallback, useState } from "react";
|
|
||||||
import styles from "./HomePage.module.css";
|
|
||||||
import { Panel } from "../../components/Panel/Panel";
|
|
||||||
import { useCast, usePlayerStatus, useReset } from "../../api/arcast";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Button } from "../../components/Button/Button";
|
|
||||||
|
|
||||||
export const HomePage: FunctionComponent = () => {
|
|
||||||
const playerStatusQuery = usePlayerStatus();
|
|
||||||
const [castUrl, setCastUrl] = useState<string>("");
|
|
||||||
const castMutation = useCast();
|
|
||||||
const resetMutation = useReset();
|
|
||||||
|
|
||||||
const onCastUrlChange = useCallback(
|
|
||||||
(evt: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setCastUrl(evt.target.value);
|
|
||||||
},
|
|
||||||
[setCastUrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCastClick = useCallback(() => {
|
|
||||||
if (!castUrl) return;
|
|
||||||
castMutation.mutate({ url: castUrl });
|
|
||||||
setCastUrl("");
|
|
||||||
}, [castUrl]);
|
|
||||||
|
|
||||||
const onResetClick = useCallback(() => {
|
|
||||||
resetMutation.mutate();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<Panel>
|
|
||||||
<h1 className={styles.title}>
|
|
||||||
<abbr title="Arcast Player Manager">A.P.M.</abbr>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className={styles.columns}>
|
|
||||||
<div>
|
|
||||||
<h2 className={styles.title}>Status</h2>
|
|
||||||
<p className={styles.statusBlock}>
|
|
||||||
<b>Instance ID: </b>
|
|
||||||
<br />
|
|
||||||
<code>{playerStatusQuery.data?.id}</code>
|
|
||||||
</p>
|
|
||||||
<p className={styles.statusBlock}>
|
|
||||||
<b>Current status: </b>
|
|
||||||
<br />
|
|
||||||
<code>{playerStatusQuery.data?.status}</code>
|
|
||||||
</p>
|
|
||||||
<p className={styles.statusBlock}>
|
|
||||||
<b>Current page title: </b>
|
|
||||||
<br />
|
|
||||||
<span>"{playerStatusQuery.data?.title}"</span>
|
|
||||||
</p>
|
|
||||||
<p className={styles.statusBlock}>
|
|
||||||
<b>Current page URL: </b>
|
|
||||||
<br />
|
|
||||||
<a target="_blank" href={playerStatusQuery.data?.url ?? "#"}>
|
|
||||||
{playerStatusQuery.data?.url
|
|
||||||
? playerStatusQuery.data?.url.slice(0, 32) + "..."
|
|
||||||
: "--"}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className={styles.title}>Control</h2>
|
|
||||||
<div className={styles.controlBlock}>
|
|
||||||
<div className={styles.castField}>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
placeholder="http://example.net"
|
|
||||||
value={castUrl}
|
|
||||||
onChange={onCastUrlChange}
|
|
||||||
/>
|
|
||||||
<button onClick={onCastClick}>Cast</button>
|
|
||||||
<button onClick={onResetClick}>Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className={styles.separator} />
|
|
||||||
<h2 className={styles.title}>Tools</h2>
|
|
||||||
<Button>
|
|
||||||
<Link to="/screen-sharing">Screen sharing</Link>
|
|
||||||
</Button>
|
|
||||||
</Panel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,8 +0,0 @@
|
|||||||
.root {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelTitle {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
import { FunctionComponent, useCallback, useEffect, useState } from "react";
|
|
||||||
import styles from "./ScreenSharingPage.module.css";
|
|
||||||
import { Panel } from "../../components/Panel/Panel";
|
|
||||||
import { Link, useParams } from "react-router-dom";
|
|
||||||
import { ScreenViewer } from "./ScreenViewer";
|
|
||||||
import { Button } from "../../components/Button/Button";
|
|
||||||
import {
|
|
||||||
ScreenSharingServer,
|
|
||||||
ScreenSharingServerState,
|
|
||||||
StateChangedEvent,
|
|
||||||
StateChangedEventType,
|
|
||||||
} from "../../api/webrtc";
|
|
||||||
|
|
||||||
export const ScreenSharingPage: FunctionComponent = () => {
|
|
||||||
const { sessionId } = useParams<{ sessionId: string }>();
|
|
||||||
const [server, setServer] = useState<ScreenSharingServer | null>(null);
|
|
||||||
const [state, setState] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const onServerStateChanged = useCallback((evt: Event) => {
|
|
||||||
setState((evt as StateChangedEvent).state);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
console.log("Server state", state);
|
|
||||||
|
|
||||||
const onShareScreenClick = useCallback(() => {
|
|
||||||
const server = new ScreenSharingServer();
|
|
||||||
server.addEventListener(StateChangedEventType, onServerStateChanged);
|
|
||||||
server.shareScreen({
|
|
||||||
video: true,
|
|
||||||
audio: false,
|
|
||||||
});
|
|
||||||
setServer(server);
|
|
||||||
return () => {
|
|
||||||
server.removeEventListener(StateChangedEventType, onServerStateChanged);
|
|
||||||
server.close();
|
|
||||||
setServer(null);
|
|
||||||
};
|
|
||||||
}, [onServerStateChanged]);
|
|
||||||
|
|
||||||
const onStopSharingClick = useCallback(() => {
|
|
||||||
if (!server) return;
|
|
||||||
server.close();
|
|
||||||
setServer(null);
|
|
||||||
setState(null);
|
|
||||||
}, [server]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!server) return;
|
|
||||||
return () => {
|
|
||||||
if (!server) return;
|
|
||||||
console.log("Closing screen sharing server");
|
|
||||||
server.close();
|
|
||||||
setServer(null);
|
|
||||||
};
|
|
||||||
}, [server]);
|
|
||||||
|
|
||||||
if (sessionId !== undefined) {
|
|
||||||
return <ScreenViewer sessionId={sessionId} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<Panel>
|
|
||||||
<Link to="/">Back to homepage</Link>
|
|
||||||
<h1 className={styles.panelTitle}>Screen sharing</h1>
|
|
||||||
{state === ScreenSharingServerState.Sharing ? (
|
|
||||||
<Button onClick={onStopSharingClick}>Stop sharing</Button>
|
|
||||||
) : (
|
|
||||||
<Button onClick={onShareScreenClick}>Share screen</Button>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
.root {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
import {
|
|
||||||
FunctionComponent,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import styles from "./ScreenViewer.module.css";
|
|
||||||
import {
|
|
||||||
ScreenSharingClient,
|
|
||||||
StateChangedEvent,
|
|
||||||
StateChangedEventType,
|
|
||||||
StreamChangedEvent,
|
|
||||||
StreamChangedEventType,
|
|
||||||
} from "../../api/webrtc";
|
|
||||||
|
|
||||||
export interface ScreenViewerProps {
|
|
||||||
sessionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ScreenViewer: FunctionComponent<ScreenViewerProps> = ({
|
|
||||||
sessionId,
|
|
||||||
}) => {
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
|
||||||
const [client, setClient] = useState<ScreenSharingClient | null>(null);
|
|
||||||
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
||||||
const [state, setState] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const onStreamChanged = useCallback((evt: Event) => {
|
|
||||||
setStream((evt as StreamChangedEvent).stream);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onStateChanged = useCallback((evt: Event) => {
|
|
||||||
setState((evt as StateChangedEvent).state);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const client = new ScreenSharingClient(sessionId);
|
|
||||||
|
|
||||||
client.addEventListener(StreamChangedEventType, onStreamChanged);
|
|
||||||
client.addEventListener(StateChangedEventType, onStateChanged);
|
|
||||||
client.connect();
|
|
||||||
setClient(client);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client.removeEventListener(StreamChangedEventType, onStreamChanged);
|
|
||||||
client.removeEventListener(StateChangedEventType, onStateChanged);
|
|
||||||
client.close();
|
|
||||||
setClient(null);
|
|
||||||
};
|
|
||||||
}, [sessionId, onStreamChanged, onStateChanged]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!videoRef.current) return;
|
|
||||||
|
|
||||||
const video = videoRef.current;
|
|
||||||
|
|
||||||
console.log("Changing video stream", video, stream);
|
|
||||||
|
|
||||||
video.autoplay = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
video.srcObject = stream;
|
|
||||||
video.muted = true;
|
|
||||||
}, [videoRef, stream]);
|
|
||||||
|
|
||||||
const isVideoReady = state === "connected" && stream !== null;
|
|
||||||
|
|
||||||
console.log("isVideoReady", isVideoReady);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<h1
|
|
||||||
style={{
|
|
||||||
display: `${isVideoReady ? "none" : "block"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ConnectionState state={state} />
|
|
||||||
</h1>
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
className={styles.video}
|
|
||||||
style={{
|
|
||||||
display: `${isVideoReady ? "block" : "none"}`,
|
|
||||||
}}
|
|
||||||
></video>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ConnectionStateProps {
|
|
||||||
state: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConnectionState: FunctionComponent<ConnectionStateProps> = ({
|
|
||||||
state,
|
|
||||||
}) => {
|
|
||||||
switch (state) {
|
|
||||||
case null:
|
|
||||||
return <span>Connecting...</span>;
|
|
||||||
case "Connecté":
|
|
||||||
return <span>Connection established !</span>;
|
|
||||||
case "disconnected":
|
|
||||||
return <span>Connection lost !</span>;
|
|
||||||
case "failed":
|
|
||||||
return <span>Connection failed !</span>;
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
État inconnu (<code>{state}</code>)
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
1
apps/main/src/react-app-env.d.ts
vendored
1
apps/main/src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|
66
apps/remote-control/app.js
Normal file
66
apps/remote-control/app.js
Normal 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()
|
36
apps/remote-control/index.html
Normal file
36
apps/remote-control/index.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!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="../lib/arcast.js" defer></script>
|
||||||
|
<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>
|
10
apps/remote-control/manifest.json
Normal file
10
apps/remote-control/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
200
apps/screen-sharing/app.js
Normal file
200
apps/screen-sharing/app.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
const displayMediaOptions = {
|
||||||
|
video: {
|
||||||
|
displaySurface: "browser",
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
suppressLocalAudioPlayback: false,
|
||||||
|
},
|
||||||
|
preferCurrentTab: false,
|
||||||
|
selfBrowserSurface: "exclude",
|
||||||
|
systemAudio: "include",
|
||||||
|
surfaceSwitching: "include",
|
||||||
|
monitorTypeSurfaces: "include",
|
||||||
|
};
|
||||||
|
|
||||||
|
const peerConnection = new RTCPeerConnection();
|
||||||
|
const signaling = Arcast.getBroadcastingChannel(
|
||||||
|
"screen-sharing",
|
||||||
|
onSignalReceived
|
||||||
|
);
|
||||||
|
|
||||||
|
var secret = window.crypto.randomUUID();
|
||||||
|
var receivedAnswer = false;
|
||||||
|
var receivedOffer = false;
|
||||||
|
var isOffering = false;
|
||||||
|
|
||||||
|
peerConnection.onconnectionstatechange = (evt) => {
|
||||||
|
console.log("Connection state changed", evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.oniceconnectionstatechange = () => {
|
||||||
|
console.log("ICE state: ", peerConnection.iceConnectionState);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.onicecandidate = (evt) => {
|
||||||
|
signaling.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "icecandidate",
|
||||||
|
data: {
|
||||||
|
candidate: evt.candidate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.ontrack = function (e) {
|
||||||
|
var screenplay = document.getElementById("screenplay");
|
||||||
|
if (!screenplay) {
|
||||||
|
screenplay = document.createElement("video");
|
||||||
|
screenplay.id = "screenplay";
|
||||||
|
document.body.appendChild(screenplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
screenplay.autoplay = true;
|
||||||
|
screenplay.playsInline = true;
|
||||||
|
screenplay.srcObject = e.streams[0];
|
||||||
|
screenplay.muted = true;
|
||||||
|
|
||||||
|
e.track.onended = (e) => (screenplay.srcObject = screenplay.srcObject);
|
||||||
|
};
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.has("secret")) {
|
||||||
|
secret = urlParams.get("secret");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareScreen() {
|
||||||
|
return Arcast.getInfo().then((info) => {
|
||||||
|
return navigator.mediaDevices
|
||||||
|
.getDisplayMedia(displayMediaOptions)
|
||||||
|
.then((captureStream) => {
|
||||||
|
isOffering = true;
|
||||||
|
const videoTrack = captureStream.getVideoTracks()[0];
|
||||||
|
peerConnection.addTrack(videoTrack, captureStream);
|
||||||
|
|
||||||
|
peerConnection.createOffer().then((offer) => {
|
||||||
|
peerConnection.setLocalDescription(offer).then(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (receivedAnswer) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signaling.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "offer",
|
||||||
|
data: {
|
||||||
|
secret: secret,
|
||||||
|
offer: offer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const url =
|
||||||
|
"http://127.0.0.1:" +
|
||||||
|
info.port +
|
||||||
|
"/apps/screen-sharing/?secret=" +
|
||||||
|
encodeURIComponent(secret);
|
||||||
|
fetch("/api/v1/cast", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSignalReceived(data) {
|
||||||
|
const message = JSON.parse(data);
|
||||||
|
switch (message.type) {
|
||||||
|
case "offer":
|
||||||
|
if (message.data.secret !== secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivedOffer || isOffering) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.setRemoteDescription(
|
||||||
|
new RTCSessionDescription(message.data.offer),
|
||||||
|
() => {
|
||||||
|
peerConnection.createAnswer(function (answer) {
|
||||||
|
peerConnection.setLocalDescription(
|
||||||
|
answer,
|
||||||
|
function () {
|
||||||
|
signaling.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "answer",
|
||||||
|
data: {
|
||||||
|
secret: secret,
|
||||||
|
answer: answer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}, error);
|
||||||
|
},
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
receivedOffer = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "answer":
|
||||||
|
if (receivedAnswer || !isOffering) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.data.secret !== secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.setRemoteDescription(
|
||||||
|
new RTCSessionDescription(message.data.answer),
|
||||||
|
() => {},
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
receivedAnswer = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "icecandidate":
|
||||||
|
if (message.data.candidate) {
|
||||||
|
peerConnection.addIceCandidate(message.data.candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log("Received unhandled message", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
30
apps/screen-sharing/index.html
Normal file
30
apps/screen-sharing/index.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!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="/apps/lib/arcast.js" defer></script>
|
||||||
|
<script type="text/javascript" src="app.js" defer></script>
|
||||||
|
<style>
|
||||||
|
#screenplay {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
object-fit: contain;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
object-fit: fill;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main" class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h1>Screen sharing</h1>
|
||||||
|
<button onclick="shareScreen()">Start</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
apps/screen-sharing/manifest.json
Normal file
10
apps/screen-sharing/manifest.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"fr": "Partage d'écran",
|
||||||
|
"en": "Screen sharing"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"fr": "Partager son écran",
|
||||||
|
"en": "Share your screen"
|
||||||
|
}
|
||||||
|
}
|
@ -5,13 +5,10 @@ import (
|
|||||||
"forge.cadoles.com/arcad/arcast/internal/command/client"
|
"forge.cadoles.com/arcad/arcast/internal/command/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
command.Main(
|
command.Main(
|
||||||
"arcast",
|
"arcast",
|
||||||
"cli client",
|
"Arcast cli client",
|
||||||
Version,
|
|
||||||
client.Root().Subcommands...,
|
client.Root().Subcommands...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,10 @@ import (
|
|||||||
"forge.cadoles.com/arcad/arcast/internal/command/player"
|
"forge.cadoles.com/arcad/arcast/internal/command/player"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
command.Main(
|
command.Main(
|
||||||
"arcast",
|
"arcast",
|
||||||
"desktop player",
|
"Arcast desktop player",
|
||||||
Version,
|
|
||||||
player.Root().Subcommands...,
|
player.Root().Subcommands...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,53 @@
|
|||||||
|
//go:build android
|
||||||
|
// +build android
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"sync"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast"
|
"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/config"
|
"forge.cadoles.com/arcad/arcast/pkg/selfsigned"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/server"
|
"forge.cadoles.com/arcad/arcast/pkg/server"
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"github.com/gioui-plugins/gio-plugins/plugin"
|
"github.com/gioui-plugins/gio-plugins/plugin"
|
||||||
|
"github.com/gioui-plugins/gio-plugins/safedata"
|
||||||
|
"github.com/gioui-plugins/gio-plugins/safedata/giosafedata"
|
||||||
|
"github.com/gioui-plugins/gio-plugins/webviewer/webview"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const packageName = "com.cadoles.arcast_player"
|
const instanceIDSecretIdentifier = "instance_id"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
webview.SetDebug(true)
|
||||||
window := app.NewWindow(
|
window := app.NewWindow(
|
||||||
app.Fullscreen.Option(),
|
app.Fullscreen.Option(),
|
||||||
app.AnyOrientation.Option(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
browser := gioui.NewBrowser(window)
|
browser := gioui.NewBrowser(window)
|
||||||
|
|
||||||
|
var safeDataConfig safedata.Config
|
||||||
|
var safeDataConfigWaigGroup sync.WaitGroup
|
||||||
|
var initSafeDataConfig sync.Once
|
||||||
|
|
||||||
|
safeDataConfigWaigGroup.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ops := new(op.Ops)
|
ops := new(op.Ops)
|
||||||
for {
|
for {
|
||||||
evt := window.NextEvent()
|
evt := window.NextEvent()
|
||||||
plugin.Install(window, evt)
|
plugin.Install(window, evt)
|
||||||
|
|
||||||
switch evt := evt.(type) {
|
switch evt := evt.(type) {
|
||||||
case system.DestroyEvent:
|
case system.DestroyEvent:
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -43,7 +55,12 @@ func main() {
|
|||||||
case system.FrameEvent:
|
case system.FrameEvent:
|
||||||
gtx := layout.NewContext(ops, evt)
|
gtx := layout.NewContext(ops, evt)
|
||||||
browser.Layout(gtx)
|
browser.Layout(gtx)
|
||||||
evt.Frame(ops)
|
evt.Frame(gtx.Ops)
|
||||||
|
case app.ViewEvent:
|
||||||
|
initSafeDataConfig.Do(func() {
|
||||||
|
defer safeDataConfigWaigGroup.Done()
|
||||||
|
safeDataConfig = giosafedata.NewConfigFromViewEvent(window, evt, "com.cadoles.arcast_player")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -54,33 +71,26 @@ func main() {
|
|||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conf := config.DefaultConfig()
|
safeDataConfigWaigGroup.Wait()
|
||||||
configFiles := getConfigFiles(ctx)
|
safe := safedata.NewSafeData(safeDataConfig)
|
||||||
for _, f := range configFiles {
|
|
||||||
logger.Info(ctx, "loading or creating configuration file", logger.F("filename", f))
|
|
||||||
if err := config.LoadOrCreate(ctx, f, conf, config.DefaultTransforms...); err != nil {
|
|
||||||
logger.Error(ctx, "could not load configuration file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
instanceID, err := getInstanceIDFromSafeData(ctx, safe)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key)
|
cert, err := selfsigned.NewLANCert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(ctx, "could not parse x509 certificate", logger.CapturedE(errors.WithStack(err)))
|
logger.Fatal(ctx, "could not generate self signed certificate", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
server := server.New(
|
server := server.New(
|
||||||
browser,
|
browser,
|
||||||
server.WithInstanceID(conf.InstanceID),
|
server.WithInstanceID(instanceID),
|
||||||
server.WithAppsEnabled(conf.Apps.Enabled),
|
server.WithAppsEnabled(true),
|
||||||
server.WithDefaultApp(conf.Apps.DefaultApp),
|
server.WithDefaultApp("home"),
|
||||||
server.WithApps(arcast.DefaultApps...),
|
server.WithApps(arcast.DefaultApps...),
|
||||||
server.WithTLSCertificate(&cert),
|
server.WithTLSCertificate(cert),
|
||||||
server.WithAddress(conf.HTTP.Address),
|
|
||||||
server.WithTLSAddress(conf.HTTPS.Address),
|
|
||||||
server.WithAllowedOrigins(conf.AllowedOrigins...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
@ -102,19 +112,26 @@ func main() {
|
|||||||
app.Main()
|
app.Main()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigFiles(ctx context.Context) []string {
|
func getInstanceIDFromSafeData(ctx context.Context, safe *safedata.SafeData) (string, error) {
|
||||||
configFiles := make([]string, 0)
|
instanceIDSecret, err := safe.Get(instanceIDSecretIdentifier)
|
||||||
|
if err != nil && err.Error() != "not found" {
|
||||||
sharedStorageConfigFile := filepath.Join("/storage/emulated/0/Android/data", packageName, "files/config.json")
|
logger.Error(ctx, "could not retrieve instance id secret", logger.CapturedE(errors.WithStack(err)))
|
||||||
configFiles = append(configFiles, sharedStorageConfigFile)
|
|
||||||
|
|
||||||
dataDir, err := app.DataDir()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not retrieve app data dir", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
} else {
|
|
||||||
appDataConfigFile := filepath.Join(dataDir, "config.json")
|
|
||||||
configFiles = append(configFiles, appDataConfigFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return configFiles
|
var instanceID string
|
||||||
|
if len(instanceIDSecret.Data) > 0 {
|
||||||
|
instanceID = string(instanceIDSecret.Data)
|
||||||
|
} else {
|
||||||
|
instanceID = server.NewRandomInstanceID()
|
||||||
|
|
||||||
|
instanceIDSecret.Identifier = instanceIDSecretIdentifier
|
||||||
|
instanceIDSecret.Data = []byte(instanceID)
|
||||||
|
instanceIDSecret.Description = "Arcast player instance identifier"
|
||||||
|
|
||||||
|
if err := safe.Set(instanceIDSecret); err != nil {
|
||||||
|
return "", errors.Wrapf(err, "could not save instance id secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instanceID, nil
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,12 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultApps []server.App
|
DefaultApps []server.App
|
||||||
//go:embed apps/main/build/**
|
//go:embed apps/**
|
||||||
appsFS embed.FS
|
appsFS embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
defaultApps, err := loadApps(
|
defaultApps, err := loadApps("apps/*")
|
||||||
"apps/main/build",
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
@ -27,11 +25,25 @@ func init() {
|
|||||||
DefaultApps = defaultApps
|
DefaultApps = defaultApps
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadApps(appDirs ...string) ([]server.App, error) {
|
func loadApps(dirPattern string) ([]server.App, error) {
|
||||||
apps := make([]server.App, 0)
|
apps := make([]server.App, 0)
|
||||||
|
|
||||||
for _, dir := range appDirs {
|
files, err := fs.Glob(appsFS, dirPattern)
|
||||||
rawManifest, err := fs.ReadFile(appsFS, filepath.Join(dir, "arcast-app.json"))
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -42,7 +54,9 @@ func loadApps(appDirs ...string) ([]server.App, error) {
|
|||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fs, err := fs.Sub(appsFS, dir)
|
app.ID = filepath.Base(f)
|
||||||
|
|
||||||
|
fs, err := fs.Sub(appsFS, "apps/"+app.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
@ -1,5 +0,0 @@
|
|||||||
# Documentation
|
|
||||||
|
|
||||||
- [Configuration](./configuration.md)
|
|
||||||
- [API HTTP](./http-api.md)
|
|
||||||
- [mDNS](./mdns.md)
|
|
@ -1,59 +0,0 @@
|
|||||||
# Configuration
|
|
||||||
|
|
||||||
Le server Arcast utilise un fichier de configuration permettant de personnaliser son comportement.
|
|
||||||
Par défaut, ce fichier de configuration est créé aux emplacements suivants au lancement de l'application:
|
|
||||||
|
|
||||||
- **Linux Desktop**:
|
|
||||||
- `${HOME}/.config/arcast-player/config.json`
|
|
||||||
- **Android**:
|
|
||||||
- `/storage/emulated/0/Android/data/com.cadoles.arcast_player`
|
|
||||||
- `/data/user/0/com.cadoles.arcast_player/files/config.json` (si le premier chemin n'est pas accessible en lecture/écriture)
|
|
||||||
|
|
||||||
Voici un exemple commenté du fichier de configuration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
// Identifiant de l'instance
|
|
||||||
"instanceId": "<instance_id>",
|
|
||||||
// Configuration du serveur HTTP
|
|
||||||
"http": {
|
|
||||||
// Couple <address>:<port> d'écoute
|
|
||||||
// Par défaut ":" i.e. toutes les adresses avec port aléatoire
|
|
||||||
"address": ":",
|
|
||||||
// Répertoire de personnalisation de la page d'accueil
|
|
||||||
// Voir section "Personnalisation" ci-dessous
|
|
||||||
"customDir": "${CONFIG_DIR}/custom"
|
|
||||||
},
|
|
||||||
// Configuration du serveur HTTPS
|
|
||||||
"https": {
|
|
||||||
// Couple <address>:<port> d'écoute
|
|
||||||
// Par défaut ":" i.e. toutes les adresses avec port aléatoire
|
|
||||||
"address": ":",
|
|
||||||
// Certificat x509 (format PEM, encodé en base64)
|
|
||||||
"cert": "<base64_encoded_pem_cert>",
|
|
||||||
// Clé privée du certificat (format PEM, encodé en base64)
|
|
||||||
"key": "<base64_encoded_pem_key>",
|
|
||||||
// Configuration de la génération du certificat autosigné
|
|
||||||
"selfSigned": {
|
|
||||||
// Activer/désactiver la génération du certificat auto-signé
|
|
||||||
"enabled": true,
|
|
||||||
// Empreinte réseau associée au certificat autosigné
|
|
||||||
// Permet de détecter si il y a eu un changement sur les adresses IPs du player
|
|
||||||
"networkFingerprint": "40f4cd914c6163657fe4e37128f90b501b768221c77b5dbfa35ca74a84b8732a"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Configuration des applications embarquées
|
|
||||||
"apps": {
|
|
||||||
// Activer/désactiver les applications embarquées
|
|
||||||
"enabled": true,
|
|
||||||
// Application par défaut
|
|
||||||
"defaultApp": "main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Personnalisation
|
|
||||||
|
|
||||||
Il est possible de personnaliser la page d'accueil du player Arcast en créant des fichiers dans le répertoire définit par l'attribut de configuration `http.customDir`.
|
|
||||||
|
|
||||||
Le contenu de ce répertoire doit répliquer l'arborescence embarquée par défaut (voir https://forge.cadoles.com/arcad/arcast/src/branch/develop/pkg/server/embed). Chaque fichier présent remplacera celui embarqué par défaut.
|
|
4
go.mod
4
go.mod
@ -4,8 +4,7 @@ go 1.21.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
gioui.org v0.4.1
|
gioui.org v0.4.1
|
||||||
github.com/dschmidt/go-layerfs v0.1.0
|
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
|
||||||
github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d
|
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
|
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
|
||||||
@ -28,6 +27,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // 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/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect
|
github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
11
go.sum
11
go.sum
@ -32,10 +32,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dschmidt/go-layerfs v0.1.0 h1:jE6aHDfjNzS/31DS48th6EkmELwTa1Uf+aO4jRkBs3U=
|
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c h1:naFDaf0CvDEYZ3Zpxx20DY/cCvBQqKwsV7ZzBt3M/bU=
|
||||||
github.com/dschmidt/go-layerfs v0.1.0/go.mod h1:m62aff0hn23Q/tQBRiNSeLD7EUuimDvsuCvCpzBr3Gw=
|
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-20240323070753-3331d8c2df5d h1:8b7owUJ8sNmgqEk+1d7ylr3TCH3vliCvY/6ycfize8o=
|
|
||||||
github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d/go.mod h1:3XVleuCdPpdajFL+ASh2wmXZNskitXQQ4jhVss0VHZg=
|
|
||||||
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 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
@ -64,6 +62,8 @@ github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a h1:uZklbtdSPrDL
|
|||||||
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a/go.mod h1:LPI3Qojj7OgTyc2R4RPB6BuMSgjoOXCObwnDzz1SOVk=
|
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a/go.mod h1:LPI3Qojj7OgTyc2R4RPB6BuMSgjoOXCObwnDzz1SOVk=
|
||||||
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
|
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
|
||||||
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
|
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc=
|
github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
@ -88,8 +88,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2mIk+KphcH6hagbVXhcFkbTgYleTI=
|
|
||||||
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
|
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
@ -146,6 +144,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -10,12 +10,11 @@ import (
|
|||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Main(name string, usage string, version string, commands ...*cli.Command) {
|
func Main(name string, usage string, commands ...*cli.Command) {
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: name,
|
Name: name,
|
||||||
Usage: usage,
|
Usage: usage,
|
||||||
Commands: commands,
|
Commands: commands,
|
||||||
Version: version,
|
|
||||||
Before: func(ctx *cli.Context) error {
|
Before: func(ctx *cli.Context) error {
|
||||||
workdir := ctx.String("workdir")
|
workdir := ctx.String("workdir")
|
||||||
// Switch to new working directory if defined
|
// Switch to new working directory if defined
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package player
|
package player
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast"
|
"forge.cadoles.com/arcad/arcast"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser/dummy"
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
|
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/config"
|
"forge.cadoles.com/arcad/arcast/pkg/selfsigned"
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/server"
|
"forge.cadoles.com/arcad/arcast/pkg/server"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@ -19,15 +15,9 @@ import (
|
|||||||
|
|
||||||
func Run() *cli.Command {
|
func Run() *cli.Command {
|
||||||
defaults := lorca.NewOptions()
|
defaults := lorca.NewOptions()
|
||||||
|
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "config",
|
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_CONFIG"},
|
|
||||||
Value: config.DefaultConfigFile(context.Background()),
|
|
||||||
},
|
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "additional-chrome-arg",
|
Name: "additional-chrome-arg",
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS"},
|
EnvVars: []string{"ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS"},
|
||||||
@ -44,8 +34,8 @@ func Run() *cli.Command {
|
|||||||
Value: ":",
|
Value: ":",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "https-address",
|
Name: "tls-address",
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_HTTPS_ADDRESS"},
|
EnvVars: []string{"ARCAST_DESKTOP_TLS_ADDRESS"},
|
||||||
Value: ":",
|
Value: ":",
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
@ -63,107 +53,55 @@ func Run() *cli.Command {
|
|||||||
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
|
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
|
||||||
Value: defaults.Width,
|
Value: defaults.Width,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "allowed-origins",
|
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_ALLOWED_ORIGINS"},
|
|
||||||
Value: cli.NewStringSlice(),
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "custom-files-dir",
|
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_CUSTOM_FILES_DIR"},
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "dummy-browser",
|
|
||||||
EnvVars: []string{"ARCAST_DESKTOP_DUMMY_BROWSER"},
|
|
||||||
Value: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
configFile := ctx.String("config")
|
|
||||||
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")...)
|
||||||
dummyBrowser := ctx.Bool("dummy-browser")
|
enableApps := ctx.Bool("apps")
|
||||||
|
serverAddress := ctx.String("address")
|
||||||
|
serverTLSAddress := ctx.String("tls-address")
|
||||||
|
|
||||||
var browser browser.Browser
|
browser := lorca.NewBrowser(
|
||||||
|
lorca.WithAdditionalChromeArgs(chromeArgs...),
|
||||||
|
lorca.WithWindowSize(windowWidth, windowHeight),
|
||||||
|
)
|
||||||
|
|
||||||
if dummyBrowser {
|
if err := browser.Start(); err != nil {
|
||||||
logger.Info(ctx.Context, "using dummy browser")
|
return errors.Wrap(err, "could not start browser")
|
||||||
browser = dummy.NewBrowser()
|
|
||||||
} else {
|
|
||||||
lorcaBrowser := lorca.NewBrowser(
|
|
||||||
lorca.WithAdditionalChromeArgs(chromeArgs...),
|
|
||||||
lorca.WithWindowSize(windowWidth, windowHeight),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := lorcaBrowser.Start(); err != nil {
|
|
||||||
return errors.Wrap(err, "could not start browser")
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
lorcaBrowser.Wait()
|
|
||||||
logger.Warn(ctx.Context, "browser was closed")
|
|
||||||
os.Exit(1)
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
logger.Info(ctx.Context, "stopping browser")
|
|
||||||
if err := lorcaBrowser.Stop(); err != nil {
|
|
||||||
logger.Error(ctx.Context, "could not stop browser", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
browser = lorcaBrowser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := config.DefaultConfig()
|
go func() {
|
||||||
|
browser.Wait()
|
||||||
|
logger.Warn(ctx.Context, "browser was closed")
|
||||||
|
os.Exit(1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logger.Info(ctx.Context, "stopping browser")
|
||||||
|
if err := browser.Stop(); err != nil {
|
||||||
|
logger.Error(ctx.Context, "could not stop browser", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
instanceID := ctx.String("instance-id")
|
instanceID := ctx.String("instance-id")
|
||||||
if instanceID != "" {
|
if instanceID == "" {
|
||||||
conf.InstanceID = instanceID
|
instanceID = server.NewRandomInstanceID()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSet("apps") {
|
cert, err := selfsigned.NewLANCert()
|
||||||
conf.Apps.Enabled = ctx.Bool("apps")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet("address") {
|
|
||||||
conf.HTTP.Address = ctx.String("address")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet("https-address") {
|
|
||||||
conf.HTTPS.Address = ctx.String("tls-address")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet("allowed-origins") {
|
|
||||||
conf.AllowedOrigins = ctx.StringSlice("allowed-origins")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet("custom-dir") {
|
|
||||||
conf.HTTP.CustomDir = ctx.String("custom-dir")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile))
|
|
||||||
if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil {
|
|
||||||
logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not parse tls cert/key pair")
|
return errors.Wrap(err, "could not generate self signed certificate")
|
||||||
}
|
}
|
||||||
|
|
||||||
server := server.New(browser,
|
server := server.New(browser,
|
||||||
server.WithInstanceID(conf.InstanceID),
|
server.WithInstanceID(instanceID),
|
||||||
server.WithAppsEnabled(conf.Apps.Enabled),
|
server.WithAppsEnabled(enableApps),
|
||||||
server.WithDefaultApp(conf.Apps.DefaultApp),
|
server.WithDefaultApp("home"),
|
||||||
server.WithApps(arcast.DefaultApps...),
|
server.WithApps(arcast.DefaultApps...),
|
||||||
server.WithAddress(conf.HTTP.Address),
|
server.WithAddress(serverAddress),
|
||||||
server.WithTLSAddress(conf.HTTPS.Address),
|
server.WithTLSAddress(serverTLSAddress),
|
||||||
server.WithTLSCertificate(&cert),
|
server.WithTLSCertificate(cert),
|
||||||
server.WithAllowedOrigins(conf.AllowedOrigins...),
|
|
||||||
server.WithUpperLayerDir(conf.HTTP.CustomDir),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
@ -1,7 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Version=1.0
|
|
||||||
Name=Arcast Player
|
|
||||||
Comment=Web diffusion server
|
|
||||||
Path=/usr/bin/arcast-player
|
|
||||||
Icon=arcast-player
|
|
@ -1,82 +0,0 @@
|
|||||||
### Source https://github.com/DesktopECHO/T95-H616-Malware
|
|
||||||
###
|
|
||||||
### Clobber Malware/Junk - Run in PowerShell or Bash
|
|
||||||
### Requires ADB Connection (ie: adb connect 10.13.12.11:5555)
|
|
||||||
###
|
|
||||||
### Install ADB (If not already installed)
|
|
||||||
###
|
|
||||||
### - Assuming you're on Windows, to install ADB simply
|
|
||||||
### install Chocolatey and install ADB using Choco:
|
|
||||||
### choco install adb
|
|
||||||
###
|
|
||||||
### - macOS users have Homebrew to accomplish the same thing:
|
|
||||||
### brew install android-platform-tools
|
|
||||||
###
|
|
||||||
|
|
||||||
wget https://github.com/bromite/bromite/releases/latest/download/arm_ChromePublic.apk -O chromium.apk
|
|
||||||
wget https://github.com/TeamAmaze/AmazeFileManager/releases/latest/download/app-fdroid-release.apk -O filemanager.apk
|
|
||||||
wget https://raw.githubusercontent.com/make-github-pseudonymous-again/pixels/main/1x1%23000000.jpg -O BlackBackground.jpg
|
|
||||||
wget https://f-droid.org/repo/com.termux_118.apk -O termux.apk
|
|
||||||
wget https://github.com/DesktopECHO/Pi-hole-for-Android/releases/latest/download/pideploy.apk -O pideploy.apk
|
|
||||||
adb push BlackBackground.jpg /sdcard/Download/BlackBackground.jpg
|
|
||||||
|
|
||||||
adb root
|
|
||||||
sleep 10
|
|
||||||
adb shell "cmd uimode night yes"
|
|
||||||
adb install filemanager.apk
|
|
||||||
adb install termux.apk
|
|
||||||
adb install pideploy.apk
|
|
||||||
adb install chromium.apk
|
|
||||||
adb shell pm uninstall --user 0 com.adups.fota
|
|
||||||
adb shell pm uninstall --user 0 com.ftest
|
|
||||||
adb shell pm uninstall --user 0 com.ionitech.airscreen
|
|
||||||
adb shell pm uninstall --user 0 com.charon.rocketfly
|
|
||||||
adb shell pm uninstall --user 0 com.netflix.mediaclient
|
|
||||||
adb shell pm uninstall --user 0 com.android.music
|
|
||||||
adb shell pm uninstall --user 0 com.android.dynsystem
|
|
||||||
adb shell pm uninstall --user 0 com.android.egg
|
|
||||||
adb shell pm uninstall --user 0 com.www.intallapp
|
|
||||||
adb shell pm uninstall --user 0 com.www.productdeclare
|
|
||||||
adb shell pm uninstall --user 0 org.xbmc.kodi
|
|
||||||
adb shell pm uninstall --user 0 com.softwinner.TvdFileManager
|
|
||||||
adb shell pm uninstall --user 0 com.apkpure.aegon
|
|
||||||
adb shell pm uninstall --user 0 com.amazon.avod.thirdpartyclient
|
|
||||||
adb shell pm uninstall --user 0 com.adups.market
|
|
||||||
adb shell pm uninstall --user 0 com.android.camera2
|
|
||||||
adb shell pm uninstall --user 0 com.android.deskclock
|
|
||||||
adb shell pm uninstall --user 0 com.android.chrome
|
|
||||||
adb shell pm uninstall --user 0 com.google.android.feedback
|
|
||||||
adb shell pm uninstall --user 0 com.google.android.katniss
|
|
||||||
adb shell pm uninstall --user 0 net.lightflash.wallpapersn
|
|
||||||
adb shell pm uninstall --user 0 com.google.android.youtube.tv
|
|
||||||
adb shell pm uninstall --user 0 com.android.gallery3d
|
|
||||||
adb shell pm uninstall --user 0 com.android.wallpaper.livepicker
|
|
||||||
adb shell pm uninstall --user 0 com.android.wallpaperbackup
|
|
||||||
adb shell pm uninstall --user 0 com.android.dreams.basic
|
|
||||||
adb shell pm uninstall --user 0 com.android.dreams.web
|
|
||||||
adb shell pm uninstall --user 0 com.android.dreams.phototable
|
|
||||||
adb shell pm uninstall --user 0 cm.aptoidetv.pt
|
|
||||||
|
|
||||||
adb shell rm -rf /data/data/org.xbmc.kodi
|
|
||||||
adb shell rm -rf /data/data/com.www.intallapp
|
|
||||||
adb shell rm -rf /data/data/com.www.productdeclare
|
|
||||||
adb shell rm -rf /data/media/0/Android/data/com.adups.fota
|
|
||||||
adb shell rm -rf /data/misc/profiles/ref/com.adups.fota
|
|
||||||
adb shell rm -rf /data/misc/profiles/cur/0/com.adups.fota
|
|
||||||
|
|
||||||
adb shell rm -rf /data/system/shared_prefs
|
|
||||||
adb shell mkdir -p /data/system/shared_prefs/open_preference.xml
|
|
||||||
adb shell chmod 0000 /data/system/shared_prefs/open_preference.xml
|
|
||||||
adb shell /vendor/bin/busybox chattr +i /data/system/shared_prefs/open_preference.xml
|
|
||||||
|
|
||||||
adb shell rm -rf /data/system/Corejava
|
|
||||||
adb shell touch /data/system/Corejava
|
|
||||||
adb shell chmod 0000 /data/system/Corejava
|
|
||||||
adb shell /vendor/bin/busybox chattr +i /data/system/Corejava
|
|
||||||
|
|
||||||
adb shell rm -rf /data/data/com.adups.fota
|
|
||||||
adb shell touch /data/data/com.adups.fota
|
|
||||||
adb shell chmod 0000 /data/data/com.adups.fota
|
|
||||||
adb shell /vendor/bin/busybox chattr +i /data/data/com.adups.fota
|
|
||||||
|
|
||||||
echo Cleanup finished, reboot your device.
|
|
@ -1,9 +1,6 @@
|
|||||||
{
|
|
||||||
prep: make build-apps
|
|
||||||
}
|
|
||||||
|
|
||||||
**/*.go
|
**/*.go
|
||||||
pkg/server/embed/**
|
pkg/server/templates/**.gotmpl
|
||||||
|
apps/**
|
||||||
modd.conf
|
modd.conf
|
||||||
.env {
|
.env {
|
||||||
prep: make build-client
|
prep: make build-client
|
||||||
|
@ -2,7 +2,7 @@ package gioui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
@ -16,20 +16,26 @@ type Browser struct {
|
|||||||
window *app.Window
|
window *app.Window
|
||||||
tag int
|
tag int
|
||||||
|
|
||||||
url *atomic.Value
|
url string
|
||||||
changed *atomic.Bool
|
changed bool
|
||||||
status *atomic.Value
|
|
||||||
title *atomic.Value
|
status browser.Status
|
||||||
|
title string
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Browser) Layout(gtx layout.Context) layout.Dimensions {
|
func (b *Browser) Layout(gtx layout.Context) {
|
||||||
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
|
||||||
events := gtx.Events(&b.tag)
|
events := gtx.Events(&b.tag)
|
||||||
for _, evt := range events {
|
for _, evt := range events {
|
||||||
switch ev := evt.(type) {
|
switch ev := evt.(type) {
|
||||||
case webviewer.TitleEvent:
|
case webviewer.TitleEvent:
|
||||||
b.title.Store(ev.Title)
|
b.title = ev.Title
|
||||||
case webviewer.NavigationEvent:
|
case webviewer.NavigationEvent:
|
||||||
b.url.Store(ev.URL)
|
b.url = ev.URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,20 +58,21 @@ func (b *Browser) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
},
|
},
|
||||||
}.Add(gtx.Ops)
|
}.Add(gtx.Ops)
|
||||||
|
|
||||||
if b.changed.CompareAndSwap(true, false) {
|
if b.changed {
|
||||||
url := b.url.Load().(string)
|
logger.Debug(ctx, "url changed", logger.F("url", b.url))
|
||||||
logger.Debug(ctx, "url changed", logger.F("url", url))
|
webviewer.NavigateOp{URL: b.url}.Add(gtx.Ops)
|
||||||
webviewer.NavigateOp{URL: url}.Add(gtx.Ops)
|
b.changed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load implements browser.Browser.
|
// Load implements browser.Browser.
|
||||||
func (b *Browser) Load(url string) error {
|
func (b *Browser) Load(url string) error {
|
||||||
b.url.Store(url)
|
b.mutex.Lock()
|
||||||
b.changed.Store(true)
|
defer b.mutex.Unlock()
|
||||||
b.status.Store(browser.StatusCasting)
|
|
||||||
|
b.url = url
|
||||||
|
b.changed = true
|
||||||
|
b.status = browser.StatusCasting
|
||||||
|
|
||||||
b.window.Invalidate()
|
b.window.Invalidate()
|
||||||
|
|
||||||
@ -74,24 +81,36 @@ func (b *Browser) Load(url string) error {
|
|||||||
|
|
||||||
// Status implements browser.Browser.
|
// Status implements browser.Browser.
|
||||||
func (b *Browser) Status() (browser.Status, error) {
|
func (b *Browser) Status() (browser.Status, error) {
|
||||||
return b.status.Load().(browser.Status), nil
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
|
||||||
|
return b.status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title implements browser.Browser.
|
// Title implements browser.Browser.
|
||||||
func (b *Browser) Title() (string, error) {
|
func (b *Browser) Title() (string, error) {
|
||||||
return b.title.Load().(string), nil
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
|
||||||
|
return b.title, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL implements browser.Browser.
|
// URL implements browser.Browser.
|
||||||
func (b *Browser) URL() (string, error) {
|
func (b *Browser) URL() (string, error) {
|
||||||
return b.url.Load().(string), nil
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
|
||||||
|
return b.url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset implements browser.Browser.
|
// Reset implements browser.Browser.
|
||||||
func (b *Browser) Reset(url string) error {
|
func (b *Browser) Reset(url string) error {
|
||||||
b.url.Store(url)
|
b.mutex.Lock()
|
||||||
b.changed.Store(true)
|
defer b.mutex.Unlock()
|
||||||
b.status.Store(browser.StatusIdle)
|
|
||||||
|
b.url = url
|
||||||
|
b.changed = true
|
||||||
|
b.status = browser.StatusIdle
|
||||||
|
|
||||||
b.window.Invalidate()
|
b.window.Invalidate()
|
||||||
|
|
||||||
@ -99,20 +118,12 @@ func (b *Browser) Reset(url string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBrowser(window *app.Window) *Browser {
|
func NewBrowser(window *app.Window) *Browser {
|
||||||
b := &Browser{
|
return &Browser{
|
||||||
window: window,
|
window: window,
|
||||||
url: &atomic.Value{},
|
url: "",
|
||||||
changed: &atomic.Bool{},
|
changed: true,
|
||||||
status: &atomic.Value{},
|
status: browser.StatusIdle,
|
||||||
title: &atomic.Value{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.url.Store("")
|
|
||||||
b.title.Store("")
|
|
||||||
b.changed.Store(false)
|
|
||||||
b.status.Store(browser.StatusIdle)
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ browser.Browser = &Browser{}
|
var _ browser.Browser = &Browser{}
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/server"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
InstanceID string `json:"instanceId"`
|
|
||||||
HTTP HTTPConfig `json:"http"`
|
|
||||||
HTTPS HTTPSConfig `json:"https"`
|
|
||||||
Apps AppsConfig `json:"apps"`
|
|
||||||
AllowedOrigins []string `json:"allowedOrigins"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPConfig struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
CustomDir string `json:"customDir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSConfig struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
Cert []byte `json:"cert"`
|
|
||||||
Key []byte `json:"key"`
|
|
||||||
SelfSigned SelfSignedCertConfig `json:"selfSigned"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelfSignedCertConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
NetworkFingerprint string `json:"networkFingerprint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppsConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
DefaultApp string `json:"defaultApp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransformFunc func(ctx context.Context, filename string, conf *Config) error
|
|
||||||
|
|
||||||
func DefaultConfigFile(ctx context.Context) string {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not get user config dir", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
configDir = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if configDir != "" {
|
|
||||||
configDir = filepath.Join(configDir, "arcast-player")
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(configDir, "config.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadOrCreate(ctx context.Context, filename string, conf *Config, funcs ...TransformFunc) error {
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
if err := json.Unmarshal(data, conf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fn := range funcs {
|
|
||||||
if err := fn(ctx, filename, conf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err = json.MarshalIndent(conf, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dirname := filepath.Dir(filename)
|
|
||||||
|
|
||||||
if _, err := os.Stat(dirname); os.IsNotExist(err) {
|
|
||||||
if err := os.MkdirAll(dirname, 0777); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(filename, data, 0640); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
InstanceID: server.NewRandomInstanceID(),
|
|
||||||
AllowedOrigins: []string{},
|
|
||||||
HTTP: HTTPConfig{
|
|
||||||
Address: ":45555",
|
|
||||||
CustomDir: "",
|
|
||||||
},
|
|
||||||
HTTPS: HTTPSConfig{
|
|
||||||
Address: ":45556",
|
|
||||||
SelfSigned: SelfSignedCertConfig{
|
|
||||||
Enabled: true,
|
|
||||||
NetworkFingerprint: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Apps: AppsConfig{
|
|
||||||
Enabled: true,
|
|
||||||
DefaultApp: "main",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateCustomDir(ctx context.Context, filename string, conf *Config) error {
|
|
||||||
if conf.HTTP.CustomDir != "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir := filepath.Dir(filename)
|
|
||||||
customFilesDir := filepath.Join(configDir, "custom")
|
|
||||||
|
|
||||||
if err := os.MkdirAll(customFilesDir, 0755); err != nil {
|
|
||||||
logger.Error(ctx, "could not create custom files directory", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.HTTP.CustomDir = customFilesDir
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
var DefaultTransforms = []TransformFunc{
|
|
||||||
GenerateSelfSignedCert,
|
|
||||||
RenewExpiredSelfSignedCert,
|
|
||||||
CreateCustomDir,
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/network"
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/selfsigned"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateSelfSignedCert(ctx context.Context, filename string, conf *Config) error {
|
|
||||||
if !conf.HTTPS.SelfSigned.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.HTTPS.Cert != nil && conf.HTTPS.Key != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rawCert, rawKey, err := selfsigned.NewLANKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not generate self signed x509 key pair")
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.HTTPS.Cert = rawCert
|
|
||||||
conf.HTTPS.Key = rawKey
|
|
||||||
|
|
||||||
networkFingerprint, err := getNetworkFingerprint()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not retrieve network fingerprint")
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.HTTPS.SelfSigned.NetworkFingerprint = networkFingerprint
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenewExpiredSelfSignedCert(ctx context.Context, filename string, conf *Config) error {
|
|
||||||
if !conf.HTTPS.SelfSigned.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.HTTPS.Cert == nil || conf.HTTPS.Key == nil {
|
|
||||||
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not parse x509 cert/key pair")
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not parse x509 certificate, regenerating one", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
|
|
||||||
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that self-signed certificate is still valid
|
|
||||||
if time.Now().Before(leaf.NotAfter) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Warn(ctx, "self-signed certificate has expired, regenerating one", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
|
|
||||||
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNetworkFingerprint() (string, error) {
|
|
||||||
ips, err := network.GetLANIPv4Addrs()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(ips)
|
|
||||||
|
|
||||||
hash := sha256.New()
|
|
||||||
for _, ip := range ips {
|
|
||||||
if _, err := hash.Write([]byte(ip)); err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
|
||||||
}
|
|
@ -19,24 +19,15 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLANKeyPair() ([]byte, []byte, error) {
|
func NewLANCert() (*tls.Certificate, error) {
|
||||||
hosts, err := network.GetLANIPv4Addrs()
|
hosts, err := network.GetLANIPv4Addrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts = append(hosts, "127.0.0.1")
|
hosts = append(hosts, "127.0.0.1")
|
||||||
|
|
||||||
rawCert, rawKey, err := NewCertKeyPair(hosts...)
|
rawCert, rawKey, err := NewCertKeyPair(hosts...)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawCert, rawKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLANCert() (*tls.Certificate, error) {
|
|
||||||
rawCert, rawKey, err := NewLANKeyPair()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -13,33 +13,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
upgrader = websocket.Upgrader{}
|
||||||
channels = &channelMap{
|
channels = &channelMap{
|
||||||
index: make(map[string]map[*websocket.Conn]struct{}),
|
index: make(map[string]map[*websocket.Conn]struct{}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) checkOrigin(r *http.Request) bool {
|
|
||||||
allowedOrigins, err := s.getAllowedOrigins()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(r.Context(), "could not retrieve allowed origins", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
requestOrigin := r.Header.Get("Origin")
|
|
||||||
|
|
||||||
for _, origin := range allowedOrigins {
|
|
||||||
if requestOrigin == origin || origin == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleBroadcast(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleBroadcast(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
c, err := s.upgrader.Upgrade(w, r, nil)
|
c, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("upgrade:", err)
|
log.Print("upgrade:", err)
|
||||||
return
|
return
|
||||||
@ -58,13 +41,9 @@ func (s *Server) handleBroadcast(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
messageType, message, err := c.ReadMessage()
|
messageType, message, err := c.ReadMessage()
|
||||||
if err != nil && !websocket.IsCloseError(err, 1001) {
|
if err != nil {
|
||||||
logger.Error(ctx, "could not read message", logger.E(errors.WithStack(err)))
|
logger.Error(ctx, "could not read message", logger.E(errors.WithStack(err)))
|
||||||
return
|
break
|
||||||
}
|
|
||||||
|
|
||||||
if messageType == -1 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug(ctx, "broadcasting message", logger.F("message", message), logger.F("messageType", messageType))
|
logger.Debug(ctx, "broadcasting message", logger.F("message", message), logger.F("messageType", messageType))
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
{{ define "base" }}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/logo.png" />
|
|
||||||
<title>Ready to cast !</title>
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
{{ block "head" .
|
|
||||||
}}{{
|
|
||||||
end
|
|
||||||
}}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="panel">
|
|
||||||
<div id="icon"></div>
|
|
||||||
{{ block "message" . }}
|
|
||||||
<h2 id="title" class="text-centered">Ready to cast !</h2>
|
|
||||||
{{ end }}
|
|
||||||
{{ block "info" . }}
|
|
||||||
<p><b>Instance ID</b></p>
|
|
||||||
<p class="text-small text-centered">
|
|
||||||
<code>{{ .ID }}</code>
|
|
||||||
</p>
|
|
||||||
<p><b>Addresses</b></p>
|
|
||||||
<ul class="text-italic text-small text-centered">
|
|
||||||
{{ $port := .Port }}
|
|
||||||
{{
|
|
||||||
range.IPs
|
|
||||||
}}
|
|
||||||
<li>
|
|
||||||
<code>{{ . }}:{{ $port }}</code>
|
|
||||||
</li>
|
|
||||||
{{
|
|
||||||
end
|
|
||||||
}}
|
|
||||||
</ul>
|
|
||||||
{{ end }}
|
|
||||||
{{if .Apps }}
|
|
||||||
{{ block "apps" . }}
|
|
||||||
<p><b>Apps</b></p>
|
|
||||||
<ul class="text-italic text-small text-centered">
|
|
||||||
{{ $tlsPort := .TLSPort }}
|
|
||||||
{{
|
|
||||||
range.IPs
|
|
||||||
}}
|
|
||||||
<li>
|
|
||||||
<a href="https://{{ . }}:{{ $tlsPort }}/apps">
|
|
||||||
https://{{ . }}:{{ $tlsPort }}/apps
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{
|
|
||||||
end
|
|
||||||
}}
|
|
||||||
</ul>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
@ -1,3 +0,0 @@
|
|||||||
{{ define "index" }}
|
|
||||||
{{ template "base" . }}
|
|
||||||
{{ end }}
|
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -1,121 +0,0 @@
|
|||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgb(76, 96, 188);
|
|
||||||
background: linear-gradient(
|
|
||||||
415deg,
|
|
||||||
rgba(4, 168, 243, 1),
|
|
||||||
rgb(76, 136, 188, 1),
|
|
||||||
rgba(76, 96, 188, 1),
|
|
||||||
rgb(115, 76, 188, 1),
|
|
||||||
rgb(87, 76, 188, 1)
|
|
||||||
);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradient 15s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*: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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: block;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 10px 10px 10px #33333361;
|
|
||||||
position: relative;
|
|
||||||
padding: 50px 30px 30px 30px;
|
|
||||||
min-width: 50%;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
|
||||||
margin: 10px 0px 20px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#icon {
|
|
||||||
width: 100px;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center center;
|
|
||||||
background-image: url("logo.png");
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -50px;
|
|
||||||
margin-top: -100px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel p,
|
|
||||||
.panel ul {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-centered {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-small {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt {
|
|
||||||
margin-top: 1em;
|
|
||||||
display: block;
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/network"
|
|
||||||
"github.com/dschmidt/go-layerfs"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed embed/**
|
|
||||||
embedFS embed.FS
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) initLayeredFS() error {
|
|
||||||
layers := make([]fs.FS, 0)
|
|
||||||
|
|
||||||
if s.upperLayerDir != "" {
|
|
||||||
upperLayer := os.DirFS(s.upperLayerDir)
|
|
||||||
layers = append(layers, upperLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseLayer, err := fs.Sub(embedFS, "embed")
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
layers = append(layers, baseLayer)
|
|
||||||
|
|
||||||
s.layeredFS = layerfs.New(layers...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
if strings.HasPrefix(path, "/_templates") {
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeFileFS(w, r, s.layeredFS, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
||||||
type templateData struct {
|
|
||||||
IPs []string
|
|
||||||
Port int
|
|
||||||
TLSPort int
|
|
||||||
ID string
|
|
||||||
Apps bool
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, err := network.GetLANIPv4Addrs()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d := templateData{
|
|
||||||
ID: s.instanceID,
|
|
||||||
IPs: ips,
|
|
||||||
Port: s.port,
|
|
||||||
TLSPort: s.tlsPort,
|
|
||||||
Apps: s.appsEnabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
templates, err := template.New("").ParseFS(s.layeredFS, "_partials/*.gohtml", "_templates/*.gohtml")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(r.Context(), "could not parse template", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(w, "index", d); err != nil {
|
|
||||||
logger.Error(r.Context(), "could not render index page", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -13,17 +14,39 @@ import (
|
|||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed templates/idle.html.gotmpl
|
||||||
|
rawIdleTemplate []byte
|
||||||
|
idleTemplate *template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tmpl, err := template.New("").Parse(string(rawIdleTemplate))
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not parse idle template"))
|
||||||
|
}
|
||||||
|
|
||||||
|
idleTemplate = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) startWebServers(ctx context.Context) error {
|
func (s *Server) startWebServers(ctx context.Context) error {
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
allowedOrigins, err := s.getAllowedOrigins()
|
if s.appsEnabled {
|
||||||
if err != nil {
|
ips, err := network.GetLANIPv4Addrs()
|
||||||
return errors.WithStack(err)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if len(allowedOrigins) > 0 {
|
|
||||||
router.Use(cors.Handler(cors.Options{
|
router.Use(cors.Handler(cors.Options{
|
||||||
AllowedOrigins: allowedOrigins,
|
AllowedOrigins: allowedOrigins,
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
@ -32,6 +55,7 @@ func (s *Server) startWebServers(ctx context.Context) error {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.Get("/", s.handleHome)
|
||||||
router.Get("/api/v1/info", s.handleInfo)
|
router.Get("/api/v1/info", s.handleInfo)
|
||||||
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)
|
||||||
@ -44,9 +68,6 @@ func (s *Server) startWebServers(ctx context.Context) error {
|
|||||||
router.Handle("/api/v1/broadcast/{channelID}", http.HandlerFunc(s.handleBroadcast))
|
router.Handle("/api/v1/broadcast/{channelID}", http.HandlerFunc(s.handleBroadcast))
|
||||||
}
|
}
|
||||||
|
|
||||||
router.Get("/", s.handleIndex)
|
|
||||||
router.Get("/*", s.handleStatic)
|
|
||||||
|
|
||||||
if err := s.startHTTPServer(ctx, router); err != nil {
|
if err := s.startHTTPServer(ctx, router); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -157,23 +178,31 @@ func (s *Server) startHTTPSServer(ctx context.Context, router chi.Router) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getAllowedOrigins() ([]string, error) {
|
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
allowedOrigins := make([]string, 0)
|
type templateData struct {
|
||||||
|
IPs []string
|
||||||
if s.appsEnabled {
|
Port int
|
||||||
ips, err := network.GetLANIPv4Addrs()
|
TLSPort int
|
||||||
if err != nil {
|
ID string
|
||||||
return nil, errors.WithStack(err)
|
Apps bool
|
||||||
}
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
allowedOrigins = append(allowedOrigins, fmt.Sprintf("http://%s:%d", ip, s.port))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.allowedOrigins) > 0 {
|
ips, err := network.GetLANIPv4Addrs()
|
||||||
allowedOrigins = append(allowedOrigins, s.allowedOrigins...)
|
if err != nil {
|
||||||
|
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return allowedOrigins, nil
|
d := templateData{
|
||||||
|
ID: s.instanceID,
|
||||||
|
IPs: ips,
|
||||||
|
Port: s.port,
|
||||||
|
TLSPort: s.tlsPort,
|
||||||
|
Apps: s.appsEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := idleTemplate.Execute(w, d); err != nil {
|
||||||
|
logger.Error(r.Context(), "could not render idle page", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,7 @@ type Options struct {
|
|||||||
EnableServiceDiscovery bool
|
EnableServiceDiscovery bool
|
||||||
EnableApps bool
|
EnableApps bool
|
||||||
DefaultApp string
|
DefaultApp string
|
||||||
AllowedOrigins []string
|
|
||||||
Apps []App
|
Apps []App
|
||||||
UpperLayerDir string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(opts *Options)
|
type OptionFunc func(opts *Options)
|
||||||
@ -41,9 +39,7 @@ func NewOptions(funcs ...OptionFunc) *Options {
|
|||||||
EnableServiceDiscovery: true,
|
EnableServiceDiscovery: true,
|
||||||
EnableApps: false,
|
EnableApps: false,
|
||||||
DefaultApp: "",
|
DefaultApp: "",
|
||||||
AllowedOrigins: make([]string, 0),
|
|
||||||
Apps: make([]App, 0),
|
Apps: make([]App, 0),
|
||||||
UpperLayerDir: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range funcs {
|
for _, fn := range funcs {
|
||||||
@ -71,12 +67,6 @@ func WithApps(apps ...App) OptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAllowedOrigins(origins ...string) OptionFunc {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.AllowedOrigins = origins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithAddress(addr string) OptionFunc {
|
func WithAddress(addr string) OptionFunc {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Address = addr
|
opts.Address = addr
|
||||||
@ -107,12 +97,6 @@ func WithServiceDiscoveryEnabled(enabled bool) OptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithUpperLayerDir(dir string) OptionFunc {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.UpperLayerDir = dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRandomInstanceID() string {
|
func NewRandomInstanceID() string {
|
||||||
return newRandomInstanceID()
|
return newRandomInstanceID()
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,8 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
@ -25,27 +23,16 @@ type Server struct {
|
|||||||
|
|
||||||
serviceDiscoveryEnabled bool
|
serviceDiscoveryEnabled bool
|
||||||
|
|
||||||
appsEnabled bool
|
appsEnabled bool
|
||||||
defaultApp string
|
defaultApp string
|
||||||
allowedOrigins []string
|
apps []App
|
||||||
apps []App
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
upgrader websocket.Upgrader
|
|
||||||
|
|
||||||
layeredFS fs.FS
|
|
||||||
upperLayerDir string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
ctx := context.Background()
|
serverCtx, cancelServer := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if err := s.initLayeredFS(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverCtx, cancelServer := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
s.cancel = cancelServer
|
s.cancel = cancelServer
|
||||||
s.ctx = serverCtx
|
s.ctx = serverCtx
|
||||||
@ -91,23 +78,15 @@ func (s *Server) Wait() error {
|
|||||||
func New(browser browser.Browser, funcs ...OptionFunc) *Server {
|
func New(browser browser.Browser, funcs ...OptionFunc) *Server {
|
||||||
opts := NewOptions(funcs...)
|
opts := NewOptions(funcs...)
|
||||||
|
|
||||||
server := &Server{
|
return &Server{
|
||||||
browser: browser,
|
browser: browser,
|
||||||
instanceID: opts.InstanceID,
|
instanceID: opts.InstanceID,
|
||||||
address: opts.Address,
|
address: opts.Address,
|
||||||
tlsAddress: opts.TLSAddress,
|
tlsAddress: opts.TLSAddress,
|
||||||
tlsCert: opts.TLSCertificate,
|
tlsCert: opts.TLSCertificate,
|
||||||
appsEnabled: opts.EnableApps,
|
appsEnabled: opts.EnableApps,
|
||||||
allowedOrigins: opts.AllowedOrigins,
|
|
||||||
defaultApp: opts.DefaultApp,
|
defaultApp: opts.DefaultApp,
|
||||||
apps: opts.Apps,
|
apps: opts.Apps,
|
||||||
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
|
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
|
||||||
upperLayerDir: opts.UpperLayerDir,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.upgrader = websocket.Upgrader{
|
|
||||||
CheckOrigin: server.checkOrigin,
|
|
||||||
}
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
}
|
||||||
|
115
pkg/server/templates/idle.html.gotmpl
Normal file
115
pkg/server/templates/idle.html.gotmpl
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Arcast - Idle</title>
|
||||||
|
<style>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h1>Arcast - Idle</h1>
|
||||||
|
<p>Instance ID:</p>
|
||||||
|
<p class="text-centered text-small">
|
||||||
|
<code>{{ .ID }}</code>
|
||||||
|
</p>
|
||||||
|
<p>Addresses:</p>
|
||||||
|
<ul class="text-italic text-small">
|
||||||
|
{{ $port := .Port }}
|
||||||
|
{{range .IPs}}
|
||||||
|
<li><code>{{ . }}:{{ $port }}</code></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{if .Apps }}
|
||||||
|
<p>Apps:</p>
|
||||||
|
<ul class="text-italic text-small">
|
||||||
|
{{ $tlsPort := .TLSPort }}
|
||||||
|
{{range .IPs}}
|
||||||
|
<li><a href="https://{{ . }}:{{ $tlsPort }}/apps">https://{{ . }}:{{ $tlsPort }}/apps</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Reference in New Issue
Block a user