forked from hibas123/ScreenSharingThing
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
35ef2a8ddc | ||
|
6d482896ba | ||
|
11810e8889 | ||
|
7ffb691922 | ||
|
8aa6d43483 | ||
012d0fe823 | |||
|
3ab6fc97e9 |
@ -4,7 +4,7 @@ root = true
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,yml,mjs}]
|
||||
[*.{js,ts,json,yml,mjs,svelte}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 3
|
||||
|
36
.github/workflows/ci.yml
vendored
Normal file
36
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# .github/workflows/ci.yml
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MY_DOCKER_USERNAME: ${{ secrets.MY_DOCKER_USERNAME }}
|
||||
MY_DOCKER_PASSWORD: ${{ secrets.MY_DOCKER_PASSWORD }}
|
||||
FORCE_COLOR: 1
|
||||
steps:
|
||||
- uses: https://github.com/earthly/actions-setup@v1
|
||||
with:
|
||||
version: v0.7.0
|
||||
- uses: actions/checkout@v2
|
||||
- name: Put back the git branch into git (Earthly uses it for tagging)
|
||||
run: |
|
||||
branch=""
|
||||
if [ -n "$GITHUB_HEAD_REF" ]; then
|
||||
branch="$GITHUB_HEAD_REF"
|
||||
else
|
||||
branch="${GITHUB_REF##*/}"
|
||||
fi
|
||||
git checkout -b "$branch" || true
|
||||
- name: Docker Login
|
||||
run: docker login git.hibas.dev --username "$MY_DOCKER_USERNAME" --password "$MY_DOCKER_PASSWORD"
|
||||
- name: Earthly version
|
||||
run: earthly --version
|
||||
- name: Run build
|
||||
run: earthly --push +docker-multi
|
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,3 +1,9 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmRegistryServer: "https://npm.hibas123.de"
|
||||
nodeLinker: "node-modules"
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.1.1.cjs
|
||||
|
1
Client/.gitignore
vendored
Normal file
1
Client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist/
|
18
Client/.routify/config.js
Normal file
18
Client/.routify/config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
"pages": "src/pages",
|
||||
"sourceDir": "public",
|
||||
"routifyDir": ".routify",
|
||||
"ignore": "",
|
||||
"dynamicImports": true,
|
||||
"singleBuild": false,
|
||||
"noHashScroll": false,
|
||||
"distDir": "dist",
|
||||
"hashScroll": true,
|
||||
"extensions": [
|
||||
"html",
|
||||
"svelte",
|
||||
"md",
|
||||
"svx"
|
||||
],
|
||||
"started": "2022-01-22T20:07:08.621Z"
|
||||
}
|
41
Client/.routify/routes.js
Normal file
41
Client/.routify/routes.js
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
/**
|
||||
* @roxi/routify 2.18.4
|
||||
* File generated Sat Jan 22 2022 21:07:08 GMT+0100 (Mitteleuropäische Normalzeit)
|
||||
*/
|
||||
|
||||
export const __version = "2.18.4"
|
||||
export const __timestamp = "2022-01-22T20:07:08.628Z"
|
||||
|
||||
//buildRoutes
|
||||
import { buildClientTree } from "@roxi/routify/runtime/buildRoutes"
|
||||
|
||||
//imports
|
||||
|
||||
|
||||
//options
|
||||
export const options = {}
|
||||
|
||||
//tree
|
||||
export const _tree = {
|
||||
"name": "root",
|
||||
"filepath": "/",
|
||||
"root": true,
|
||||
"ownMeta": {},
|
||||
"absolutePath": "src/pages",
|
||||
"children": [],
|
||||
"isLayout": false,
|
||||
"isReset": false,
|
||||
"isIndex": false,
|
||||
"isFallback": false,
|
||||
"meta": {
|
||||
"recursive": true,
|
||||
"preload": false,
|
||||
"prerender": true
|
||||
},
|
||||
"path": "/"
|
||||
}
|
||||
|
||||
|
||||
export const {tree, routes} = buildClientTree(_tree)
|
||||
|
1
Client/.routify/urlIndex.json
Normal file
1
Client/.routify/urlIndex.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
1
Client/README.md
Normal file
1
Client/README.md
Normal file
@ -0,0 +1 @@
|
||||
# Client
|
32
Client/index.html
Normal file
32
Client/index.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hibas123 Screen Share</title>
|
||||
<script defer type="module" src="./src/bootstrap.ts"></script>
|
||||
|
||||
<link id="theme" rel="stylesheet" href="/smui.css" />
|
||||
|
||||
<!-- <link rel="stylesheet" href="/styles.css" /> -->
|
||||
|
||||
<!-- Material Icons -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<!-- Roboto -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,600,700" />
|
||||
<!-- Roboto Mono -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
|
||||
</html>
|
64
Client/package.json
Normal file
64
Client/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@hibas123/screen-client",
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"scripts": {
|
||||
"dev": "run-p dev:routify dev:vite",
|
||||
"dev:vite": "vite --host",
|
||||
"dev:routify": "routify",
|
||||
"build": "run-s prepare build:routify build:vite",
|
||||
"build:routify": "routify -b",
|
||||
"build:vite": "vite build",
|
||||
"prepare": "npm run smui-theme-light && npm run smui-theme-dark",
|
||||
"smui-theme-light": "smui-theme compile public/smui.css -i src/theme",
|
||||
"smui-theme-dark": "smui-theme compile public/smui-dark.css -i src/theme/dark"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hibas123/utils": "^2.2.18",
|
||||
"@material/theme": "^14.0.0",
|
||||
"@rtdb2/sdk": "^3.0.0-beta.7",
|
||||
"@smui-extra/accordion": "^7.0.0-beta.14",
|
||||
"@smui-extra/badge": "^7.0.0-beta.14",
|
||||
"@smui/banner": "^7.0.0-beta.14",
|
||||
"@smui/button": "^7.0.0-beta.14",
|
||||
"@smui/card": "^7.0.0-beta.14",
|
||||
"@smui/checkbox": "^7.0.0-beta.14",
|
||||
"@smui/circular-progress": "^7.0.0-beta.14",
|
||||
"@smui/common": "^7.0.0-beta.14",
|
||||
"@smui/data-table": "^7.0.0-beta.14",
|
||||
"@smui/dialog": "^7.0.0-beta.14",
|
||||
"@smui/drawer": "^7.0.0-beta.14",
|
||||
"@smui/fab": "^7.0.0-beta.14",
|
||||
"@smui/floating-label": "^7.0.0-beta.14",
|
||||
"@smui/icon-button": "^7.0.0-beta.14",
|
||||
"@smui/layout-grid": "^7.0.0-beta.14",
|
||||
"@smui/line-ripple": "^7.0.0-beta.14",
|
||||
"@smui/linear-progress": "^7.0.0-beta.14",
|
||||
"@smui/list": "^7.0.0-beta.14",
|
||||
"@smui/menu": "^7.0.0-beta.14",
|
||||
"@smui/menu-surface": "^7.0.0-beta.14",
|
||||
"@smui/notched-outline": "^7.0.0-beta.14",
|
||||
"@smui/paper": "^7.0.0-beta.14",
|
||||
"@smui/radio": "^7.0.0-beta.14",
|
||||
"@smui/ripple": "^7.0.0-beta.14",
|
||||
"@smui/select": "^7.0.0-beta.14",
|
||||
"@smui/snackbar": "^7.0.0-beta.14",
|
||||
"@smui/tab": "^7.0.0-beta.14",
|
||||
"@smui/tab-indicator": "^7.0.0-beta.14",
|
||||
"@smui/tab-scroller": "^7.0.0-beta.14",
|
||||
"@smui/textfield": "^7.0.0-beta.14",
|
||||
"@smui/top-app-bar": "^7.0.0-beta.14",
|
||||
"dayjs": "^1.11.9",
|
||||
"nanoid": "^3.3.6",
|
||||
"peerjs": "^1.4.7",
|
||||
"svelte": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roxi/routify": "^2.18.12",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"smui-theme": "^7.0.0-beta.14",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"vite": "^4.4.3"
|
||||
}
|
||||
}
|
29
Client/public/smui-dark.css
Normal file
29
Client/public/smui-dark.css
Normal file
File diff suppressed because one or more lines are too long
29
Client/public/smui.css
Normal file
29
Client/public/smui.css
Normal file
File diff suppressed because one or more lines are too long
5
Client/routify.config.js
Normal file
5
Client/routify.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
routifyDir: "src/.routify",
|
||||
dynamicImports: true,
|
||||
extensions: ["svelte"],
|
||||
};
|
15
Client/src/.routify/config.js
Normal file
15
Client/src/.routify/config.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
"pages": "src/pages",
|
||||
"sourceDir": "public",
|
||||
"routifyDir": "src/.routify",
|
||||
"ignore": "",
|
||||
"dynamicImports": true,
|
||||
"singleBuild": true,
|
||||
"noHashScroll": false,
|
||||
"distDir": "dist",
|
||||
"hashScroll": true,
|
||||
"extensions": [
|
||||
"svelte"
|
||||
],
|
||||
"started": "2023-07-13T13:49:48.147Z"
|
||||
}
|
57
Client/src/.routify/routes.js
Normal file
57
Client/src/.routify/routes.js
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
/**
|
||||
* @roxi/routify 2.18.12
|
||||
* File generated Thu Jul 13 2023 15:49:48 GMT+0200 (Mitteleuropäische Sommerzeit)
|
||||
*/
|
||||
|
||||
export const __version = "2.18.12"
|
||||
export const __timestamp = "2023-07-13T13:49:48.159Z"
|
||||
|
||||
//buildRoutes
|
||||
import { buildClientTree } from "@roxi/routify/runtime/buildRoutes"
|
||||
|
||||
//imports
|
||||
|
||||
|
||||
//options
|
||||
export const options = {}
|
||||
|
||||
//tree
|
||||
export const _tree = {
|
||||
"root": true,
|
||||
"children": [
|
||||
{
|
||||
"isIndex": true,
|
||||
"isPage": true,
|
||||
"path": "/index",
|
||||
"id": "_index",
|
||||
"component": () => import('../pages/index.svelte').then(m => m.default)
|
||||
},
|
||||
{
|
||||
"isDir": true,
|
||||
"ext": "",
|
||||
"children": [
|
||||
{
|
||||
"isDir": true,
|
||||
"ext": "",
|
||||
"children": [
|
||||
{
|
||||
"isIndex": true,
|
||||
"isPage": true,
|
||||
"path": "/session/:sessionid/index",
|
||||
"id": "_session__sessionid_index",
|
||||
"component": () => import('../pages/session/[sessionid]/index.svelte').then(m => m.default)
|
||||
}
|
||||
],
|
||||
"path": "/session/:sessionid"
|
||||
}
|
||||
],
|
||||
"path": "/session"
|
||||
}
|
||||
],
|
||||
"path": "/"
|
||||
}
|
||||
|
||||
|
||||
export const {tree, routes} = buildClientTree(_tree)
|
||||
|
3
Client/src/.routify/urlIndex.json
Normal file
3
Client/src/.routify/urlIndex.json
Normal file
@ -0,0 +1,3 @@
|
||||
[
|
||||
"/index"
|
||||
]
|
6
Client/src/App.svelte
Normal file
6
Client/src/App.svelte
Normal file
@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Router } from "@roxi/routify";
|
||||
import { routes } from "./.routify/routes";
|
||||
</script>
|
||||
|
||||
<Router {routes} />
|
105
Client/src/api.ts
Normal file
105
Client/src/api.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import {
|
||||
type ICollectionRef,
|
||||
type IDocumentRef,
|
||||
UpdateTypes,
|
||||
} from "@rtdb2/sdk";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Peer } from "peerjs";
|
||||
import {
|
||||
collectionToStore,
|
||||
Sessions,
|
||||
type IMember,
|
||||
type IMessage,
|
||||
type ISession,
|
||||
} from "./db";
|
||||
|
||||
import type { Readable } from "svelte/store";
|
||||
import { Signal } from "@hibas123/utils";
|
||||
|
||||
export class Session {
|
||||
#client_id = nanoid(22);
|
||||
#peer: Peer;
|
||||
|
||||
#session: IDocumentRef<ISession>;
|
||||
#members: ICollectionRef<IMember>;
|
||||
#messages: ICollectionRef<IMessage>;
|
||||
#self: IDocumentRef<IMember>;
|
||||
|
||||
#name: string;
|
||||
#iv: any;
|
||||
|
||||
#closeSignal = new Signal();
|
||||
|
||||
members: Readable<(IMember & { $id: string })[]>;
|
||||
messages: Readable<(IMessage & { $id: string })[]>;
|
||||
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
set name(value) {
|
||||
this.#name = value;
|
||||
this.#self.update({ name: UpdateTypes.Value(value) });
|
||||
}
|
||||
|
||||
constructor(id: string, name: string) {
|
||||
this.#name = name;
|
||||
|
||||
this.#peer = new Peer(this.#client_id, {
|
||||
host: window.location.hostname,
|
||||
port: window.location.port as any,
|
||||
path: "/peerjs",
|
||||
config: {},
|
||||
});
|
||||
|
||||
this.#session = Sessions.doc(id);
|
||||
this.#members = this.#session.collection("members");
|
||||
this.#messages = this.#session.collection("messages");
|
||||
this.#self = this.#members.doc(this.#client_id);
|
||||
this.members = collectionToStore(this.#members);
|
||||
this.messages = collectionToStore(this.#messages);
|
||||
|
||||
this.registerSelf();
|
||||
this.#iv = setInterval(() => this.tick(), 5000);
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.#self.update({ lastActive: UpdateTypes.Value(Date.now()) });
|
||||
}
|
||||
|
||||
registerSelf() {
|
||||
this.#self.set({
|
||||
name: this.#name,
|
||||
lastActive: Date.now(),
|
||||
offline: false,
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#closeSignal.sendSignal();
|
||||
clearInterval(this.#iv);
|
||||
this.#self.update({ offline: UpdateTypes.Value(true) });
|
||||
this.#peer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
let bitrate = new URL(window.location.href).searchParams.get("br") || 50000;
|
||||
|
||||
function bitrateTransform(sdp: string) {
|
||||
var arr = sdp.split("\r\n");
|
||||
arr.forEach((str, i) => {
|
||||
if (/^a=fmtp:\d*/.test(str)) {
|
||||
console.log("found fmtp");
|
||||
arr[i] =
|
||||
str +
|
||||
`;x-google-max-bitrate=${bitrate};x-google-min-bitrate=0;x-google-start-bitrate=12000`;
|
||||
} else if (/^a=mid:(1|video)/.test(str)) {
|
||||
console.log("found mid");
|
||||
arr[i] += "\r\nb=AS:" + bitrate;
|
||||
}
|
||||
});
|
||||
|
||||
let res = arr.join("\r\n");
|
||||
console.log(sdp, res);
|
||||
return res;
|
||||
}
|
5
Client/src/bootstrap.ts
Normal file
5
Client/src/bootstrap.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import App from "./App.svelte";
|
||||
|
||||
new App({
|
||||
target: document.body,
|
||||
});
|
43
Client/src/db.ts
Normal file
43
Client/src/db.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { AwaitStore } from "@hibas123/utils";
|
||||
import Client, { type ICollectionRef } from "@rtdb2/sdk";
|
||||
import { readable } from "svelte/store";
|
||||
|
||||
const client = new Client(
|
||||
"https://rtdb.hibas123.de",
|
||||
"screen_share",
|
||||
"screen_share"
|
||||
);
|
||||
|
||||
(window as any).db = client;
|
||||
|
||||
export const Sessions = client.collection<ISession>("sessions");
|
||||
|
||||
export interface ISession {}
|
||||
|
||||
export interface IMember {
|
||||
lastActive: number;
|
||||
offline: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
sender: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function collectionToStore<T>(coll: ICollectionRef<T>) {
|
||||
return readable<(T & { $id: string })[]>([], (set) => {
|
||||
const cancel = new AwaitStore(false);
|
||||
(async () => {
|
||||
const itr = await coll.onSnapshot();
|
||||
cancel.awaitValue(true).then(() => itr.close());
|
||||
for await (const snapshot of itr) {
|
||||
set(snapshot.docs.map((doc) => ({ ...doc.data(), $id: doc.id })));
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancel.send(true);
|
||||
};
|
||||
});
|
||||
}
|
80
Client/src/pages/index.svelte
Normal file
80
Client/src/pages/index.svelte
Normal file
@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import Button from "@smui/button";
|
||||
import Textfield from "@smui/textfield";
|
||||
import Paper, { Title as PTitle, Content as PContent } from "@smui/paper";
|
||||
import { nanoid } from "nanoid";
|
||||
import { url } from "@roxi/routify";
|
||||
import Snackbar, { Actions, Label } from "@smui/snackbar";
|
||||
import type { SnackbarComponentDev } from "@smui/snackbar";
|
||||
import IconButton from "@smui/icon-button";
|
||||
|
||||
let sessionID = "";
|
||||
let errorSnack: SnackbarComponentDev;
|
||||
|
||||
function create() {
|
||||
sessionID = nanoid(22);
|
||||
connect();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (sessionID.length < 22) {
|
||||
errorSnack.open();
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.pushState(
|
||||
undefined,
|
||||
undefined,
|
||||
$url(`/session/${sessionID}`)
|
||||
);
|
||||
sessionID = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<Paper elevation={6}>
|
||||
<PContent>
|
||||
<PTitle>Create Session</PTitle>
|
||||
<div style="margin-top: 1rem" />
|
||||
<Button variant="raised" on:click={create}>Create</Button>
|
||||
</PContent>
|
||||
</Paper>
|
||||
<Paper elevation={6}>
|
||||
<PContent>
|
||||
<PTitle>Join Session</PTitle>
|
||||
<Textfield
|
||||
label="Session ID"
|
||||
bind:value={sessionID}
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<div style="margin-top: 1rem" />
|
||||
<Button variant="raised" on:click={connect}>Connect</Button>
|
||||
</PContent>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Snackbar bind:this={errorSnack}>
|
||||
<Label>Invalid Session!</Label>
|
||||
<Actions>
|
||||
<IconButton class="material-icons" title="Dismiss">close</IconButton>
|
||||
</Actions>
|
||||
</Snackbar>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(150px, 300px));
|
||||
grid-template-rows: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
42
Client/src/pages/session/[sessionid]/index.svelte
Normal file
42
Client/src/pages/session/[sessionid]/index.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { Session } from "../../../api";
|
||||
export let sessionid: string;
|
||||
|
||||
let session = new Session(sessionid, "");
|
||||
let members = session.members;
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
session.close();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- {sessionid} -->
|
||||
|
||||
<div class="outer">
|
||||
<div class="members">
|
||||
{#each $members as member (member.$id)}
|
||||
<div class="member">
|
||||
<div class="name">{member.name || "anon"}</div>
|
||||
<div class="id">{member.$id}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="content">Content</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
</style>
|
28
Client/src/theme/_smui-theme.scss
Normal file
28
Client/src/theme/_smui-theme.scss
Normal file
@ -0,0 +1,28 @@
|
||||
@use "sass:color";
|
||||
|
||||
@use "@material/theme/color-palette";
|
||||
|
||||
// Svelte Colors!
|
||||
@use "@material/theme/index" as theme with
|
||||
(
|
||||
$primary: #1e88e5,
|
||||
$secondary: #d8701b,
|
||||
$surface: #fff,
|
||||
$background: #fff,
|
||||
$error: color-palette.$red-900
|
||||
);
|
||||
|
||||
@import "@material/typography/mdc-typography";
|
||||
|
||||
// html,
|
||||
// body {
|
||||
// background-color: theme.$surface;
|
||||
// color: theme.$on-surface;
|
||||
// }
|
||||
|
||||
// a {
|
||||
// color: #40b3ff;
|
||||
// }
|
||||
// a:visited {
|
||||
// color: color.scale(#40b3ff, $lightness: -35%);
|
||||
// }
|
28
Client/src/theme/dark/_smui-theme.scss
Normal file
28
Client/src/theme/dark/_smui-theme.scss
Normal file
@ -0,0 +1,28 @@
|
||||
@use "sass:color";
|
||||
|
||||
@use "@material/theme/color-palette";
|
||||
|
||||
// Svelte Colors! (Dark Theme)
|
||||
@use "@material/theme/index" as theme with
|
||||
(
|
||||
$primary: #1e88e5,
|
||||
$secondary: #d8701b,
|
||||
$surface: color.adjust(color-palette.$grey-900, $blue: +4),
|
||||
$background: #000,
|
||||
$error: color-palette.$red-700
|
||||
);
|
||||
|
||||
@import "@material/typography/mdc-typography";
|
||||
|
||||
// html,
|
||||
// body {
|
||||
// background-color: #000;
|
||||
// color: theme.$on-surface;
|
||||
// }
|
||||
|
||||
// a {
|
||||
// color: #40b3ff;
|
||||
// }
|
||||
// a:visited {
|
||||
// color: color.scale(#40b3ff, $lightness: -35%);
|
||||
// }
|
5
Client/svelte.config.js
Normal file
5
Client/svelte.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
const autoPreprocess = require("svelte-preprocess");
|
||||
|
||||
module.exports = {
|
||||
preprocess: autoPreprocess(),
|
||||
};
|
20
Client/tsconfig.json
Normal file
20
Client/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Node",
|
||||
// "resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "tout",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
80
Client/vite.config.mts
Normal file
80
Client/vite.config.mts
Normal file
@ -0,0 +1,80 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import * as zlib from "zlib";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ["@roxi/routify"],
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/peerjs": {
|
||||
target: "http://localhost:5000/",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(/^\/peerjs/, ""),
|
||||
},
|
||||
"*": "/index.html",
|
||||
},
|
||||
},
|
||||
plugins: [svelte()],
|
||||
build: {
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
{
|
||||
name: "pregzip-files",
|
||||
generateBundle: async function (options, bundle) {
|
||||
let prms: Promise<any>[] = [];
|
||||
for (const fileName in bundle) {
|
||||
const file = bundle[fileName];
|
||||
// if (fileName.startsWith("assets/")) {
|
||||
let src;
|
||||
if (file.type === "asset") {
|
||||
src = file.source;
|
||||
} else {
|
||||
src = file.code;
|
||||
}
|
||||
// console.log("Compressing asset:", typeof file.source == "undefined" ? file : undefined)
|
||||
|
||||
prms.push(
|
||||
new Promise<any>((yes, no) =>
|
||||
zlib.gzip(
|
||||
src,
|
||||
{
|
||||
level: 6,
|
||||
},
|
||||
(err, res) => (err ? no(err) : yes(res))
|
||||
)
|
||||
).then((gzip) => {
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
name: file.name + ".gz",
|
||||
fileName: file.fileName + ".gz",
|
||||
source: gzip,
|
||||
});
|
||||
}),
|
||||
new Promise((yes, no) =>
|
||||
zlib.brotliCompress(src, (err, res) =>
|
||||
err ? no(err) : yes(res)
|
||||
)
|
||||
).then((br) => {
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
name: file.name + ".br",
|
||||
fileName: file.fileName + ".br",
|
||||
source: br as Uint8Array,
|
||||
});
|
||||
})
|
||||
);
|
||||
// }
|
||||
}
|
||||
|
||||
await Promise.all(prms);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
17
Dockerfile
17
Dockerfile
@ -1,17 +0,0 @@
|
||||
FROM node:12
|
||||
|
||||
LABEL maintainer="Fabian Stamm <dev@fabianstamm.de>"
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN npm config set registry https://npm.hibas123.de
|
||||
|
||||
COPY ["package.json", "tsconfig.json", "/usr/src/app/"]
|
||||
|
||||
RUN npm install
|
||||
COPY src/ /usr/src/app/src
|
||||
COPY public/ /usr/src/app/public
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
CMD ["npm", "run", "start"]
|
39
Earthfile
Normal file
39
Earthfile
Normal file
@ -0,0 +1,39 @@
|
||||
VERSION 0.7
|
||||
FROM node:lts-alpine3.18
|
||||
WORKDIR /build
|
||||
|
||||
deps:
|
||||
COPY .yarnrc.yml .yarnrc.yml
|
||||
COPY .yarn .yarn
|
||||
COPY package.json .
|
||||
COPY Server/package.json Server/
|
||||
COPY Client/package.json Client/
|
||||
RUN apk add --no-cache git python3 make g++ gcc
|
||||
RUN yarn install
|
||||
|
||||
build:
|
||||
FROM +deps
|
||||
COPY Client ./Client
|
||||
COPY Server ./Server
|
||||
RUN yarn install
|
||||
RUN sh -c "find . -type f -name '*.ts' | grep -v node_modules"
|
||||
RUN yarn build
|
||||
SAVE ARTIFACT Client/dist /dist
|
||||
SAVE ARTIFACT Server/lib /lib
|
||||
|
||||
docker-multi:
|
||||
BUILD +build
|
||||
BUILD --platform linux/amd64 --platform linux/arm64 +docker
|
||||
|
||||
docker:
|
||||
FROM +deps
|
||||
|
||||
COPY +build/lib ./Server/lib
|
||||
COPY +build/dist ./Server/public
|
||||
WORKDIR /build/Server
|
||||
|
||||
ENTRYPOINT ["node", "lib/index.js"]
|
||||
|
||||
ARG EARTHLY_TARGET_TAG
|
||||
ARG TAG=$EARTHLY_TARGET_TAG
|
||||
SAVE IMAGE --push git.hibas.dev/hibas123/screenshare:$TAG
|
1
Server/.gitignore
vendored
Normal file
1
Server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
lib/
|
18
Server/package.json
Normal file
18
Server/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@hibas123/screen-server",
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts",
|
||||
"build": "tsc",
|
||||
"dev": "nodemon -e ts --exec ts-node src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"peer": "^1.0.0"
|
||||
}
|
||||
}
|
21
Server/src/index.ts
Normal file
21
Server/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ExpressPeerServer } from "peer";
|
||||
import express = require("express");
|
||||
import * as path from "path";
|
||||
|
||||
const app = express();
|
||||
|
||||
const server = app.listen(5000, () => {
|
||||
console.log("Server is running on port 5000");
|
||||
});
|
||||
|
||||
const peerServer = ExpressPeerServer(server, {
|
||||
path: "/",
|
||||
});
|
||||
|
||||
|
||||
app.use("/peerjs", peerServer as any);
|
||||
app.use("/", express.static("public"));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.resolve(__dirname, "..", "public", "index.html"));
|
||||
});
|
13
Server/tsconfig.json
Normal file
13
Server/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "lib/",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src"]
|
||||
}
|
36
package.json
36
package.json
@ -1,16 +1,24 @@
|
||||
{
|
||||
"name": "ScreenSharingThing",
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.11",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"peer": "^0.6.1"
|
||||
}
|
||||
"name": "ScreenSharingThing",
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:client": "yarn workspace @hibas123/screen-client build",
|
||||
"build:server": "yarn workspace @hibas123/screen-server build",
|
||||
"build": "run-s build:server build:client",
|
||||
"dev:client": "yarn workspace @hibas123/screen-client dev",
|
||||
"dev:server": "yarn workspace @hibas123/screen-server dev",
|
||||
"dev": "run-p dev:client dev:server"
|
||||
},
|
||||
"workspaces": [
|
||||
"Server",
|
||||
"Client"
|
||||
],
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hibas123/jrpcgen": "~1.2.13"
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SimpleStream</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
|
||||
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/peerjs@1.3.1"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>SimpleStream</h1>
|
||||
<h2>Your ID: <span id="streamId"></span></h2>
|
||||
<div id="streamURLCont">
|
||||
<h2>Connect URL: <a id="streamURL"></a></h2>
|
||||
</div>
|
||||
<div id="connectToCont">
|
||||
<h2>Connecting to: <span id="connectToID"></span></h2>
|
||||
<button id="startStramBTN">Start Stream</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<video id="localVideo" style="width: 100%" controls></video>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="main.mjs" type="module">
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
172
public/main.mjs
172
public/main.mjs
@ -1,172 +0,0 @@
|
||||
function simpleFramwork() {
|
||||
let obj = {};
|
||||
|
||||
return new Proxy(obj, {
|
||||
get(_, prop) {
|
||||
const [id, param] = prop.split("_");
|
||||
let elm = document.getElementById(id);
|
||||
if (!elm) {
|
||||
console.log(`Element with id ${id} not found`);
|
||||
return false;
|
||||
}
|
||||
switch (param) {
|
||||
case "value":
|
||||
return elm.value;
|
||||
case "href":
|
||||
return elm.href;
|
||||
case "style":
|
||||
return elm.getAttribute("style");
|
||||
case "text":
|
||||
return elm.innerText;
|
||||
case undefined:
|
||||
return elm;
|
||||
}
|
||||
},
|
||||
set(_, prop, value) {
|
||||
const [id, param] = prop.split("_");
|
||||
let elm = document.getElementById(id);
|
||||
if (!elm) {
|
||||
console.log(`Element with id ${id} not found`);
|
||||
return false;
|
||||
}
|
||||
switch (param) {
|
||||
case "value":
|
||||
elm.value = value;
|
||||
break;
|
||||
case "href":
|
||||
elm.href = value;
|
||||
break;
|
||||
case "click":
|
||||
elm.addEventListener("click", value);
|
||||
break;
|
||||
case "style":
|
||||
elm.setAttribute("style", value);
|
||||
break;
|
||||
case "text":
|
||||
elm.innerText = value;
|
||||
break;
|
||||
case undefined:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let sf = simpleFramwork();
|
||||
|
||||
sf.streamId_text = "Loading...";
|
||||
sf.streamURL_text = "Loading...";
|
||||
|
||||
let connectToId = new URL(window.location.href).searchParams.get("id");
|
||||
|
||||
let bitrate = new URL(window.location.href).searchParams.get("br") || 50000;
|
||||
|
||||
if (connectToId) {
|
||||
sf.streamURLCont_style = "display:none";
|
||||
sf.connectToCont_style = "display:block";
|
||||
sf.connectToID_text = connectToId;
|
||||
} else {
|
||||
sf.streamURLCont_style = "display:block";
|
||||
sf.connectToCont_style = "display:none";
|
||||
}
|
||||
|
||||
var peer = new Peer({
|
||||
host: window.location.hostname,
|
||||
port: window.location.port,
|
||||
path: "/peerjs",
|
||||
});
|
||||
|
||||
function bitrateTransform(sdp) {
|
||||
var arr = sdp.split("\r\n");
|
||||
arr.forEach((str, i) => {
|
||||
if (/^a=fmtp:\d*/.test(str)) {
|
||||
console.log("found fmtp");
|
||||
arr[i] =
|
||||
str +
|
||||
`;x-google-max-bitrate=${bitrate};x-google-min-bitrate=0;x-google-start-bitrate=12000`;
|
||||
} else if (/^a=mid:(1|video)/.test(str)) {
|
||||
console.log("found mid");
|
||||
arr[i] += "\r\nb=AS:" + bitrate;
|
||||
}
|
||||
});
|
||||
|
||||
let res = arr.join("\r\n");
|
||||
console.log(sdp, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
let currentStream = undefined;
|
||||
peer.on("open", (id) => {
|
||||
console.log("ID", id);
|
||||
sf.streamId_text = id;
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.set("id", id);
|
||||
sf.streamURL_text = url.href;
|
||||
sf.streamURL_href = url.href;
|
||||
|
||||
let con = peer.connect(connectToId);
|
||||
con.on("data", console.log);
|
||||
con.on("open", () => con.send("Hello"));
|
||||
|
||||
if (connectToId) {
|
||||
sf.startStramBTN_click = () => {
|
||||
navigator.mediaDevices
|
||||
.getDisplayMedia({
|
||||
video: true,
|
||||
})
|
||||
.then((stream) => {
|
||||
if(currentStream) {
|
||||
currentStream.getTracks().forEach(track => track.stop())
|
||||
}
|
||||
currentStream = stream;
|
||||
let v = sf.localVideo;
|
||||
v.srcObject = stream;
|
||||
v.play();
|
||||
const conn = peer.call(connectToId, stream, {
|
||||
sdpTransform: bitrateTransform,
|
||||
});
|
||||
conn.on("stream", console.log);
|
||||
conn.on("close", console.log);
|
||||
conn.on("error", console.log);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
peer.on("connection", (conn) => {
|
||||
console.log("connection", conn);
|
||||
conn.on("data", console.log);
|
||||
});
|
||||
|
||||
peer.on("error", console.error);
|
||||
peer.on("close", console.log);
|
||||
peer.on("disconnected", console.log);
|
||||
peer.on("error", console.log);
|
||||
|
||||
peer.on("call", (call) => {
|
||||
console.log("Call", call);
|
||||
call.answer(undefined);
|
||||
call.on("stream", console.log);
|
||||
call.on("close", console.log);
|
||||
call.on("error", console.log);
|
||||
call.on("stream", (remoteStream) => {
|
||||
console.log("stream", remoteStream);
|
||||
let v = sf.localVideo;
|
||||
v.srcObject = remoteStream;
|
||||
v.play();
|
||||
});
|
||||
});
|
||||
|
||||
function createEmptyVideoTrack({ width, height }) {
|
||||
const canvas = Object.assign(document.createElement("canvas"), {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
canvas.getContext("2d").fillRect(0, 0, width, height);
|
||||
|
||||
const stream = canvas.captureStream();
|
||||
const track = stream.getVideoTracks()[0];
|
||||
|
||||
return Object.assign(track, { enabled: false });
|
||||
}
|
11
src/index.ts
11
src/index.ts
@ -1,11 +0,0 @@
|
||||
import { ExpressPeerServer } from "peer";
|
||||
import * as express from "express";
|
||||
|
||||
const app = express();
|
||||
const server = app.listen(3000);
|
||||
const peerServer = ExpressPeerServer(server, {
|
||||
path: "/",
|
||||
});
|
||||
|
||||
app.use("/peerjs", peerServer as any);
|
||||
app.use("/", express.static("public"));
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
"moduleResolution": "node" /* Specify how the compiler resolves module names. */,
|
||||
"esModuleInterop": false /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user