Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdfb7205

Browse files
committed
init client-web
1 parent6f83e44 commitdfb7205

26 files changed

+1130
-1240
lines changed
File renamed without changes.

‎client-web/index.html‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<htmllang="en">
3+
4+
<head>
5+
<metacharset="UTF-8"/>
6+
<linkrel="icon"type="image/svg+xml"href="/vite.svg"/>
7+
<metaname="viewport"content="width=device-width, initial-scale=1.0"/>
8+
<title>Vite + Lit + TS</title>
9+
<scripttype="module"src="/src/index.ts"></script>
10+
</head>
11+
12+
<body>
13+
<my-element>
14+
<h1>Vite + Lit</h1>
15+
</my-element>
16+
</body>
17+
18+
</html>

‎client-web/package.json‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name":"client-web",
3+
"private":true,
4+
"version":"0.0.0",
5+
"type":"module",
6+
"main":"./dist/pulsebeam.umd.cjs",
7+
"module":"./dist/pulsebeam.es.js",
8+
"types":"./dist/types/index.d.ts",
9+
"exports": {
10+
".": {
11+
"require": {
12+
"types":"./dist/types/index.d.ts",
13+
"default":"./dist/pulsebeam.es.js"
14+
},
15+
"default": {
16+
"types":"./dist/types/index.d.ts",
17+
"default":"./dist/pulsebeam.umd.js"
18+
}
19+
}
20+
},
21+
"scripts": {
22+
"proto":"protoc --ts_out src/lib --proto_path proto proto/sfu.proto",
23+
"dev":"vite",
24+
"build":"tsc && vite build",
25+
"preview":"vite preview"
26+
},
27+
"dependencies": {
28+
"@protobuf-ts/runtime":"^2.10.0",
29+
"lit":"^3.3.0"
30+
},
31+
"devDependencies": {
32+
"@protobuf-ts/plugin":"^2.10.0",
33+
"typescript":"~5.8.3",
34+
"vite":"^6.3.5",
35+
"vite-plugin-dts":"^4.5.4"
36+
}
37+
}

‎client-web/proto/sfu.proto‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
syntax="proto3";
2+
3+
packagesfu;
4+
5+
// Represents the kind of media track.
6+
enumTrackKind {
7+
TRACK_KIND_UNSPECIFIED=0;
8+
VIDEO=1;
9+
AUDIO=2;
10+
}
11+
12+
// --- Client to Server Messages ---
13+
14+
messageClientSubscribePayload {
15+
stringmid=1;// The client's MID (transceiver slot) to use for this track.
16+
stringremote_track_id=2;// The application-level ID of the remote track to subscribe to.
17+
}
18+
19+
messageClientUnsubscribePayload {
20+
stringmid=1;// The client's MID (transceiver slot) to unsubscribe from.
21+
}
22+
23+
// ClientMessage encapsulates all possible messages from client to SFU.
24+
messageClientMessage {
25+
oneofpayload {
26+
ClientSubscribePayloadsubscribe=1;
27+
ClientUnsubscribePayloadunsubscribe=2;
28+
}
29+
}
30+
31+
32+
// --- Server to Client Messages ---
33+
34+
messageTrackInfo {
35+
stringtrack_id=1;// The ID of the newly available remote track.
36+
TrackKindkind=2;// The kind of track.
37+
stringparticipant_id=3;// The ID of the participant who published this track.
38+
// map<string, string> metadata = 4; // Optional: any other app-specific metadata about the track.
39+
}
40+
41+
messageTrackSwitchInfo {
42+
stringmid=2;// The client's MID that the SFU will use (confirming client's request).
43+
optionalTrackInforemote_track=3;
44+
}
45+
46+
messageTrackPublishedPayload {
47+
TrackInforemote_track=1;
48+
}
49+
50+
messageTrackUnpublishedPayload {
51+
stringremote_track_id=1;// The ID of the remote track that is no longer available.
52+
}
53+
54+
messageTrackSwitchedPayload {
55+
repeatedTrackSwitchInfoswitches=1;
56+
}
57+
58+
messageErrorPayload {
59+
stringdescription=1;// General error message from the SFU.
60+
}
61+
62+
// ServerMessage encapsulates all possible messages from SFU to client.
63+
messageServerMessage {
64+
oneofpayload {
65+
ErrorPayloaderror=1;// General error from SFU.
66+
TrackPublishedPayloadtrack_published=2;// SFU informs client a new remote track is available.
67+
TrackUnpublishedPayloadtrack_unpublished=3;// SFU informs client a remote track is no longer available.
68+
TrackSwitchedPayloadtrack_switched=4;// SFU confirms track switching for a mid
69+
}
70+
}

‎client-web/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export{MyElement}from"./my-element.ts";

‎client-web/src/lib/core.ts‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import{ClientMessage,ServerMessage}from"./sfu.ts";
2+
3+
constMAX_DOWNSTREAMS=9;
4+
5+
typeMID=string;
6+
7+
exportinterfaceClientCoreConfig{
8+
sfuUrl:string;
9+
maxDownstreams:number;
10+
onStateChanged?:(state:RTCPeerConnectionState)=>void;
11+
}
12+
13+
exportclassClientCore{
14+
#sfuUrl:string;
15+
#pc:RTCPeerConnection;
16+
#rpc:RTCDataChannel;
17+
#videoSender:RTCRtpTransceiver;
18+
#audioSender:RTCRtpTransceiver;
19+
#closed:boolean;
20+
21+
#videoSlots:Record<MID,RTCRtpTransceiver>;
22+
#audioSlots:Record<MID,RTCRtpTransceiver>;
23+
24+
constructor(cfg:ClientCoreConfig){
25+
this.#sfuUrl=cfg.sfuUrl;
26+
constmaxDownstreams=Math.max(
27+
Math.min(cfg.maxDownstreams,MAX_DOWNSTREAMS),
28+
0,
29+
);
30+
constonStateChanged=cfg.onStateChanged||(()=>{});
31+
this.#closed=false;
32+
this.#videoSlots={};
33+
this.#audioSlots={};
34+
35+
this.#pc=newRTCPeerConnection();
36+
this.#pc.onconnectionstatechange=()=>{
37+
constconnectionState=this.#pc.connectionState;
38+
console.debug(`PeerConnection state changed:${connectionState}`);
39+
if(connectionState==="connected"){
40+
onStateChanged(connectionState);
41+
}elseif(
42+
connectionState==="failed"||connectionState==="closed"||
43+
connectionState==="disconnected"
44+
){
45+
this.#close(
46+
`PeerConnection state became:${connectionState}`,
47+
);
48+
}
49+
};
50+
51+
this.#pc.ontrack=(event:RTCTrackEvent)=>{
52+
constmid=event.transceiver?.mid;
53+
consttrack=event.track;
54+
if(!mid||!track){
55+
console.warn("Received track event without MID or track object.");
56+
return;
57+
}
58+
59+
// TODO: implement this
60+
};
61+
62+
// SFU RPC DataChannel
63+
this.#rpc=this.#pc.createDataChannel("pulsebeam::rpc");
64+
this.#rpc.binaryType="arraybuffer";
65+
this.#rpc.onmessage=(event:MessageEvent)=>{
66+
try{
67+
constserverMessage=ServerMessage.fromBinary(
68+
newUint8Array(event.dataasArrayBuffer),
69+
);
70+
constpayload=serverMessage.payload;
71+
constpayloadKind=payload.oneofKind;
72+
if(!payloadKind){
73+
console.warn("Received SFU message with undefined payload kind.");
74+
return;
75+
}
76+
77+
// TODO: implement this
78+
}catch(e:any){
79+
this.#close(`Error processing SFU RPC message:${e}`);
80+
}
81+
};
82+
this.#rpc.onclose=()=>{
83+
this.#close("Internal RPC closed prematurely");
84+
};
85+
this.#rpc.onerror=(e)=>{
86+
this.#close(`Internal RPC closed prematurely with an error:${e}`);
87+
};
88+
89+
// Transceivers
90+
this.#videoSender=this.#pc.addTransceiver("video",{
91+
direction:"sendonly",
92+
});
93+
this.#audioSender=this.#pc.addTransceiver("audio",{
94+
direction:"sendonly",
95+
});
96+
for(leti=0;i<maxDownstreams;i++){
97+
constvideoTransceiver=this.#pc.addTransceiver("video",{
98+
direction:"recvonly",
99+
});
100+
if(!videoTransceiver.mid){
101+
this.#close("missing mid from video recvonly");
102+
return;
103+
}
104+
105+
this.#videoSlots[videoTransceiver.mid]=videoTransceiver;
106+
constaudioTransceiver=this.#pc.addTransceiver("audio",{
107+
direction:"recvonly",
108+
});
109+
110+
if(!audioTransceiver.mid){
111+
this.#close("missing mid from audio recvonly");
112+
return;
113+
}
114+
this.#audioSlots[audioTransceiver.mid]=audioTransceiver;
115+
}
116+
}
117+
118+
#close(error?:string){
119+
if(this.#closed)return;
120+
121+
if(error){
122+
console.error("exited with an error:",error);
123+
}
124+
125+
this.#closed=true;
126+
}
127+
128+
asyncconnect(room:string,participant:string){
129+
if(this.#closed){
130+
consterrorMessage=
131+
"This client instance has been terminated and cannot be reused.";
132+
console.error(errorMessage);
133+
thrownewError(errorMessage);// More direct feedback to developer
134+
}
135+
136+
try{
137+
constoffer=awaitthis.#pc.createOffer();
138+
awaitthis.#pc.setLocalDescription(offer);
139+
constresponse=awaitfetch(
140+
`${this.#sfuUrl}?room=${room}&participant=${participant}`,
141+
{
142+
method:"POST",
143+
body:offer.sdp!,
144+
headers:{"Content-Type":"application/sdp"},
145+
},
146+
);
147+
if(!response.ok){
148+
thrownewError(
149+
`Signaling request failed:${response.status}${awaitresponse
150+
.text()}`,
151+
);
152+
}
153+
awaitthis.#pc.setRemoteDescription({
154+
type:"answer",
155+
sdp:awaitresponse.text(),
156+
});
157+
// Status transitions to "connected" will be handled by onconnectionstatechange and data channel onopen events.
158+
}catch(error:any){
159+
this.#close(
160+
error.message||"Signaling process failed unexpectedly.",
161+
);
162+
}
163+
}
164+
165+
disconnect(){
166+
this.#pc.close();
167+
}
168+
169+
publish(stream:MediaStream){
170+
constvideoTracks=stream.getVideoTracks();
171+
if(videoTracks.length>1){
172+
thrownewError(
173+
`Unexpected MediaStream composition: Expected at most one video track, but found${videoTracks.length}. This component or function is designed to handle a single video source and/or a single audio source.`,
174+
);
175+
}
176+
177+
constaudioTracks=stream.getAudioTracks();
178+
if(audioTracks.length>1){
179+
thrownewError(
180+
`Unexpected MediaStream composition: Expected at most one audio track, but found${audioTracks.length}. This component or function is designed to handle a single audio source and/or a single audio source.`,
181+
);
182+
}
183+
184+
constnewVideoTrack=videoTracks.at(0)||null;
185+
this.#videoSender.sender.replaceTrack(newVideoTrack);
186+
187+
constnewAudioTrack=audioTracks.at(0)||null;
188+
this.#audioSender.sender.replaceTrack(newAudioTrack);
189+
}
190+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp