Skip to content

Commit eaf7efd

Browse files
committed
work on webrtc service
1 parent eb1cf89 commit eaf7efd

File tree

4 files changed

+116
-60
lines changed

4 files changed

+116
-60
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ Demonstration project using https://github.com/node-projects/web-component-desig
66

77
https://node-projects.github.io/web-component-designer-demo/index.html
88

9+
## Collaboration Notes
10+
11+
Manual WebRTC signaling works out of the box for same-browser tabs. For different machines you will often need STUN or TURN servers. The demo accepts one or more `collabIceServer` query parameters, for example:
12+
13+
```text
14+
https://node-projects.github.io/web-component-designer-demo/index.html?collabIceServer=stun:stun.l.google.com:19302
15+
```
16+
17+
For more advanced setups you can pass a full JSON-encoded `RTCConfiguration` through `collabRtcConfiguration`.
18+
919
## Developing
1020

1121
* Install dependencies

package-lock.json

Lines changed: 19 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@
3030
"@mlc-ai/web-llm": "^0.2.82",
3131
"@node-projects/base-custom-webcomponent": "0.46.0",
3232
"@node-projects/css-parser": "^5.2.0",
33-
"@node-projects/layout2vector": "^4.2.0",
33+
"@node-projects/layout2vector": "^4.6.0",
3434
"@node-projects/lean-he-esm": "^3.4.1",
3535
"@node-projects/node-html-parser-esm": "^6.4.1",
3636
"@node-projects/pickr-webcomponent": "^1.1.0",
3737
"@node-projects/web-component-designer": "^0.1.352",
3838
"@node-projects/web-component-designer-codeview-monaco": "^0.1.33",
39-
"@node-projects/web-component-designer-collaboration-service": "^0.1.3",
39+
"@node-projects/web-component-designer-collaboration-service": "^0.1.4",
4040
"@node-projects/web-component-designer-htmlparserservice-nodehtmlparser": "^0.1.12",
4141
"@node-projects/web-component-designer-stylesheetservice-css-parser": "^0.1.4",
4242
"@node-projects/web-component-designer-widgets-wunderbaum": "^0.1.44",
4343
"dock-spawn-ts": "^3.18.0",
4444
"es-module-shims": "^2.8.0",
4545
"esprima-next": "^6.0.3",
4646
"lit": "^3.3.2",
47-
"mdn-data": "^2.27.1",
47+
"mdn-data": "^2.28.0",
4848
"mobile-drag-drop": "^3.0.0-rc.0",
4949
"monaco-editor": "^0.55.1",
5050
"typescript": "^6.0.2",

src/appShell.ts

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,29 @@ function writeCollaborationSignalingChannels(channels: readonly WebRtcTabCollabo
103103
}
104104
}
105105

106+
function readCollaborationRtcConfiguration(): RTCConfiguration | undefined {
107+
const params = new URLSearchParams(window.location.search);
108+
const serializedConfiguration = params.get('collabRtcConfiguration');
109+
if (serializedConfiguration) {
110+
try {
111+
const parsedConfiguration = JSON.parse(serializedConfiguration) as RTCConfiguration;
112+
if (parsedConfiguration && typeof parsedConfiguration === 'object')
113+
return parsedConfiguration;
114+
} catch {
115+
}
116+
}
117+
118+
const iceServerUrls = params.getAll('collabIceServer')
119+
.map(value => value.trim())
120+
.filter(Boolean);
121+
if (iceServerUrls.length === 0)
122+
return undefined;
123+
124+
return {
125+
iceServers: iceServerUrls.map(url => ({ urls: url }))
126+
};
127+
}
128+
106129
import { DockManager, DockSpawnTsWebcomponent } from 'dock-spawn-ts';
107130
import { BaseCustomWebComponentConstructorAppend, css, Disposable, html, LazyLoader } from '@node-projects/base-custom-webcomponent';
108131
import { CommandHandling } from './CommandHandling.js'
@@ -274,6 +297,7 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
274297
private _npmPackageLoader = new NpmPackageLoader();
275298
private _collaborationPeerBaseId = getOrCreateCollaborationPeerBaseId();
276299
private _collaborationSignalingChannels = readCollaborationSignalingChannels();
300+
private _collaborationRtcConfiguration = readCollaborationRtcConfiguration();
277301
private _collaborationSessionOverrides = new WeakMap<DocumentContainer, string>();
278302
private _collaborationTransports = new WeakMap<DocumentContainer, WebRtcTabCollaborationTransport>();
279303
private _closeCollaborationHelpPopup?: () => void;
@@ -569,7 +593,10 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
569593
private _getOrCreateCollaborationTransport(documentContainer: DocumentContainer) {
570594
let transport = this._collaborationTransports.get(documentContainer);
571595
if (!transport) {
572-
transport = new WebRtcTabCollaborationTransport({ enabledSignalingChannels: this._collaborationSignalingChannels });
596+
transport = new WebRtcTabCollaborationTransport({
597+
enabledSignalingChannels: this._collaborationSignalingChannels,
598+
rtcConfiguration: this._collaborationRtcConfiguration,
599+
} as any);
573600
this._collaborationTransports.set(documentContainer, transport);
574601
} else {
575602
transport.setEnabledSignalingChannels(this._collaborationSignalingChannels);
@@ -670,6 +697,7 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
670697
<li>Copy the new signaling bundle in client B and paste it back in client A.</li>
671698
<li>If a client still shows a newer bundle, copy that one back once more.</li>
672699
</ol>
700+
<div style="margin-bottom: 12px;">Cross-machine WebRTC often needs STUN or TURN servers. Add <code>collabIceServer</code> query parameters, for example <code>?collabIceServer=stun:stun.l.google.com:19302</code>, or pass a full JSON config in <code>collabRtcConfiguration</code>.</div>
673701
<div style="margin-bottom: 12px;">The paste action reads from the clipboard directly when the browser allows it.</div>
674702
`;
675703

@@ -774,6 +802,11 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
774802
this.exportData('html');
775803
}
776804
},
805+
{
806+
title: 'export as EMF', action: async () => {
807+
this.exportData('emf');
808+
}
809+
},
777810
{ title: '-' },
778811
{
779812
title: 'export overlay', checked: this._exportOverlays, checkable: true, action: () => {
@@ -790,6 +823,56 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
790823
], e);
791824
}
792825

826+
async exportData(format: 'dxf' | 'pdf' | 'png' | 'svg' | 'html' | 'emf') {
827+
const { extractIR, renderIR, DXFWriter, PDFWriter, PNGWriter, SVGWriter, HTMLWriter, EMFWriter } = await import("@node-projects/layout2vector");
828+
829+
const doc = <DocumentContainer>this._dockManager.activeDocument.resolvedElementContent;
830+
831+
const source = this._exportOverlays ? [
832+
doc.designerView.designerCanvas.rootDesignItem.element,
833+
doc.designerView.designerCanvas.overlayLayer
834+
] : doc.designerView.designerCanvas.rootDesignItem.element;
835+
836+
const ir = await extractIR(source, {
837+
boxType: "border", // "border" | "content"
838+
includeText: true, // extract text node geometry
839+
includeInvisible: false, // skip display:none / visibility:hidden
840+
includeImages: true,
841+
zoom: 1 / doc.designerView.designerCanvas.zoomFactor,
842+
convertFormControls: true, // convert form controls (input, select, textarea) to rectangles with text
843+
walkIframes: true, // recursively extract iframes
844+
});
845+
if (format === 'dxf') {
846+
const dxfWriter = new DXFWriter(document.documentElement.scrollHeight);
847+
const dxfString = await renderIR(ir, dxfWriter);
848+
await saveData(dxfString, "dxfFile", 'dxf');
849+
} else if (format === 'pdf') {
850+
const pdfWriter = new PDFWriter(1000, 1000);
851+
const pdfDoc = await renderIR(ir, pdfWriter);
852+
await pdfDoc.finalize();
853+
const pdfBytes = pdfDoc.toBytes();
854+
await saveData(pdfBytes, 'pdfFile', 'pdf');
855+
} else if (format === 'png') {
856+
const pngWriter = new PNGWriter(document.documentElement.scrollWidth, document.documentElement.scrollHeight);
857+
const pngResult = await renderIR(ir, pngWriter);
858+
await pngResult.finalize();
859+
const pngBytes = pngResult.toBytes();
860+
await saveData(pngBytes, 'pngFile', 'png');
861+
} else if (format === 'svg') {
862+
const svgWriter = new SVGWriter(2000, 1000);
863+
const svgString = await renderIR(ir, svgWriter);
864+
await saveData(svgString, 'svgFile', 'svg');
865+
} else if (format === 'html') {
866+
const htmlWriter = new HTMLWriter(1000, 2000);
867+
const htmlContent = await renderIR(ir, htmlWriter);
868+
await saveData(htmlContent, 'htmlFile', 'html');
869+
} else if (format === 'emf') {
870+
const emfWriter = new EMFWriter(1000, 2000);
871+
const emfContent = await renderIR(ir, emfWriter);
872+
await saveData(emfContent, 'emfFile', 'emf');
873+
}
874+
}
875+
793876
showCollaborationContextMenu(e: MouseEvent) {
794877
const documentContainer = this._getActiveDocumentContainer();
795878
const collaborationService = documentContainer?.instanceServiceContainer.collaborationService;
@@ -907,50 +990,6 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
907990
], e);
908991
}
909992

910-
async exportData(format: 'dxf' | 'pdf' | 'png' | 'svg' | 'html') {
911-
const { extractIR, renderIR, DXFWriter, PDFWriter, PNGWriter, SVGWriter, HTMLWriter } = await import("@node-projects/layout2vector");
912-
913-
const doc = <DocumentContainer>this._dockManager.activeDocument.resolvedElementContent;
914-
915-
const source = this._exportOverlays ? [
916-
doc.designerView.designerCanvas.rootDesignItem.element,
917-
doc.designerView.designerCanvas.overlayLayer
918-
] : doc.designerView.designerCanvas.rootDesignItem.element;
919-
920-
const ir = await extractIR(source, {
921-
boxType: "border", // "border" | "content"
922-
includeText: true, // extract text node geometry
923-
includeInvisible: false, // skip display:none / visibility:hidden
924-
includeImages: true,
925-
zoom: 1 / doc.designerView.designerCanvas.zoomFactor
926-
});
927-
if (format === 'dxf') {
928-
const dxfWriter = new DXFWriter(document.documentElement.scrollHeight);
929-
const dxfString = await renderIR(ir, dxfWriter);
930-
await saveData(dxfString, "dxfFile", 'dxf');
931-
} else if (format === 'pdf') {
932-
const pdfWriter = new PDFWriter(1000, 1000);
933-
const pdfDoc = await renderIR(ir, pdfWriter);
934-
await pdfDoc.finalize();
935-
const pdfBytes = pdfDoc.toBytes();
936-
await saveData(pdfBytes, 'pdfFile', 'pdf');
937-
} else if (format === 'png') {
938-
const pngWriter = new PNGWriter(document.documentElement.scrollWidth, document.documentElement.scrollHeight);
939-
const pngResult = await renderIR(ir, pngWriter);
940-
await pngResult.finalize();
941-
const pngBytes = pngResult.toBytes();
942-
await saveData(pngBytes, 'pngFile', 'png');
943-
} else if (format === 'svg') {
944-
const svgWriter = new SVGWriter(2000, 1000);
945-
const svgString = await renderIR(ir, svgWriter);
946-
await saveData(svgString, 'svgFile', 'svg');
947-
} else if (format === 'html') {
948-
const htmlWriter = new HTMLWriter(1000, 2000);
949-
const htmlContent = await renderIR(ir, htmlWriter);
950-
await saveData(htmlContent, 'htmlFile', 'html');
951-
}
952-
}
953-
954993
engine: webllmType.MLCEngine;
955994
async LLM() {
956995
let op = this._getDomElement<HTMLTextAreaElement>('llmOutput');

0 commit comments

Comments
 (0)