Skip to content

Commit 0cc9941

Browse files
committed
add tracks on confirmation
1 parent dfb7205 commit 0cc9941

File tree

5 files changed

+114
-42
lines changed

5 files changed

+114
-42
lines changed

client-web/proto/sfu.proto

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ message TrackSwitchInfo {
4444
}
4545

4646
message TrackPublishedPayload {
47-
TrackInfo remote_track = 1;
47+
repeated TrackInfo remote_tracks = 1;
4848
}
4949

5050
message TrackUnpublishedPayload {
51-
string remote_track_id = 1; // The ID of the remote track that is no longer available.
51+
repeated string remote_track_ids = 1; // The ID of the remote track that is no longer available.
5252
}
5353

5454
message TrackSwitchedPayload {

client-web/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
11
export { MyElement } from "./my-element.ts";
2+
import { ClientCore } from "./lib";
3+
4+
(async () => {
5+
const client = new ClientCore({
6+
sfuUrl: "http://localhost:3000/",
7+
maxDownstreams: 1,
8+
});
9+
10+
await client.connect("default", `alice-${Math.round(Math.random() * 100)}`);
11+
12+
const stream = await navigator.mediaDevices.getUserMedia({
13+
video: true,
14+
});
15+
16+
client.publish(stream);
17+
})();

client-web/src/lib/core.ts

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
import { ClientMessage, ServerMessage } from "./sfu.ts";
1+
import { ClientMessage, ServerMessage, TrackInfo } from "./sfu.ts";
22

33
const MAX_DOWNSTREAMS = 9;
44

55
type MID = string;
66

7+
// Internal Ids
8+
type ParticipantId = string;
9+
type TrackId = string;
10+
11+
interface Slot {
12+
transceiver: RTCRtpTransceiver;
13+
track: MediaStreamTrack;
14+
info?: TrackInfo;
15+
}
16+
17+
interface ParticipantSlot {
18+
video?: MID;
19+
audio?: MID;
20+
}
21+
722
export interface ClientCoreConfig {
823
sfuUrl: string;
924
maxDownstreams: number;
10-
onStateChanged?: (state: RTCPeerConnectionState) => void;
1125
}
1226

1327
export class ClientCore {
@@ -18,26 +32,30 @@ export class ClientCore {
1832
#audioSender: RTCRtpTransceiver;
1933
#closed: boolean;
2034

21-
#videoSlots: Record<MID, RTCRtpTransceiver>;
22-
#audioSlots: Record<MID, RTCRtpTransceiver>;
35+
#slots: Record<MID, Slot>;
36+
#participantSlots: ParticipantSlot[];
37+
#availableTracks: Record<ParticipantId, Record<TrackId, TrackInfo>>;
38+
39+
onStateChanged = (state: RTCPeerConnectionState) => { };
40+
onTrack = (track: RTCPeerConnection) => { };
2341

2442
constructor(cfg: ClientCoreConfig) {
2543
this.#sfuUrl = cfg.sfuUrl;
2644
const maxDownstreams = Math.max(
2745
Math.min(cfg.maxDownstreams, MAX_DOWNSTREAMS),
2846
0,
2947
);
30-
const onStateChanged = cfg.onStateChanged || (() => {});
3148
this.#closed = false;
32-
this.#videoSlots = {};
33-
this.#audioSlots = {};
49+
this.#slots = {};
50+
this.#availableTracks = {};
51+
this.#participantSlots = [];
3452

3553
this.#pc = new RTCPeerConnection();
3654
this.#pc.onconnectionstatechange = () => {
3755
const connectionState = this.#pc.connectionState;
3856
console.debug(`PeerConnection state changed: ${connectionState}`);
3957
if (connectionState === "connected") {
40-
onStateChanged(connectionState);
58+
this.onStateChanged(connectionState);
4159
} else if (
4260
connectionState === "failed" || connectionState === "closed" ||
4361
connectionState === "disconnected"
@@ -51,12 +69,43 @@ export class ClientCore {
5169
this.#pc.ontrack = (event: RTCTrackEvent) => {
5270
const mid = event.transceiver?.mid;
5371
const track = event.track;
72+
const transceiver = event.transceiver;
5473
if (!mid || !track) {
55-
console.warn("Received track event without MID or track object.");
74+
this.#close("Received track event without MID or track object.");
5675
return;
5776
}
5877

59-
// TODO: implement this
78+
console.log(event);
79+
this.#slots[mid] = {
80+
track,
81+
transceiver,
82+
};
83+
84+
if (track.kind === "video") {
85+
for (const slot of this.#participantSlots) {
86+
if (!slot.video) {
87+
slot.video = mid;
88+
return;
89+
}
90+
}
91+
92+
this.#participantSlots.push({
93+
video: mid,
94+
});
95+
} else if (track.kind === "audio") {
96+
for (const slot of this.#participantSlots) {
97+
if (!slot.audio) {
98+
slot.audio = mid;
99+
return;
100+
}
101+
}
102+
103+
this.#participantSlots.push({
104+
audio: mid,
105+
});
106+
} else {
107+
console.warn("unknown track kind, ignoring:", track.kind);
108+
}
60109
};
61110

62111
// SFU RPC DataChannel
@@ -74,6 +123,15 @@ export class ClientCore {
74123
return;
75124
}
76125

126+
switch (payloadKind) {
127+
case "trackPublished":
128+
break;
129+
case "trackUnpublished":
130+
break;
131+
case "trackSwitched":
132+
break;
133+
}
134+
77135
// TODO: implement this
78136
} catch (e: any) {
79137
this.#close(`Error processing SFU RPC message: ${e}`);
@@ -93,25 +151,16 @@ export class ClientCore {
93151
this.#audioSender = this.#pc.addTransceiver("audio", {
94152
direction: "sendonly",
95153
});
154+
96155
for (let i = 0; i < maxDownstreams; i++) {
97-
const videoTransceiver = this.#pc.addTransceiver("video", {
156+
// ontrack will be fired with acknowledgement from the server
157+
this.#pc.addTransceiver("video", {
98158
direction: "recvonly",
99159
});
100-
if (!videoTransceiver.mid) {
101-
this.#close("missing mid from video recvonly");
102-
return;
103-
}
104160

105-
this.#videoSlots[videoTransceiver.mid] = videoTransceiver;
106-
const audioTransceiver = this.#pc.addTransceiver("audio", {
161+
this.#pc.addTransceiver("audio", {
107162
direction: "recvonly",
108163
});
109-
110-
if (!audioTransceiver.mid) {
111-
this.#close("missing mid from audio recvonly");
112-
return;
113-
}
114-
this.#audioSlots[audioTransceiver.mid] = audioTransceiver;
115164
}
116165
}
117166

@@ -187,4 +236,9 @@ export class ClientCore {
187236
const newAudioTrack = audioTracks.at(0) || null;
188237
this.#audioSender.sender.replaceTrack(newAudioTrack);
189238
}
239+
240+
unpublish() {
241+
this.#videoSender.sender.replaceTrack(null);
242+
this.#audioSender.sender.replaceTrack(null);
243+
}
190244
}

client-web/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ClientCore, type ClientCoreConfig } from "./core.ts";

client-web/src/lib/sfu.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,18 @@ export interface TrackSwitchInfo {
9696
*/
9797
export interface TrackPublishedPayload {
9898
/**
99-
* @generated from protobuf field: sfu.TrackInfo remote_track = 1;
99+
* @generated from protobuf field: repeated sfu.TrackInfo remote_tracks = 1;
100100
*/
101-
remoteTrack?: TrackInfo;
101+
remoteTracks: TrackInfo[];
102102
}
103103
/**
104104
* @generated from protobuf message sfu.TrackUnpublishedPayload
105105
*/
106106
export interface TrackUnpublishedPayload {
107107
/**
108-
* @generated from protobuf field: string remote_track_id = 1;
108+
* @generated from protobuf field: repeated string remote_track_ids = 1;
109109
*/
110-
remoteTrackId: string; // The ID of the remote track that is no longer available.
110+
remoteTrackIds: string[]; // The ID of the remote track that is no longer available.
111111
}
112112
/**
113113
* @generated from protobuf message sfu.TrackSwitchedPayload
@@ -466,11 +466,12 @@ export const TrackSwitchInfo = new TrackSwitchInfo$Type();
466466
class TrackPublishedPayload$Type extends MessageType<TrackPublishedPayload> {
467467
constructor() {
468468
super("sfu.TrackPublishedPayload", [
469-
{ no: 1, name: "remote_track", kind: "message", T: () => TrackInfo }
469+
{ no: 1, name: "remote_tracks", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => TrackInfo }
470470
]);
471471
}
472472
create(value?: PartialMessage<TrackPublishedPayload>): TrackPublishedPayload {
473473
const message = globalThis.Object.create((this.messagePrototype!));
474+
message.remoteTracks = [];
474475
if (value !== undefined)
475476
reflectionMergePartial<TrackPublishedPayload>(this, message, value);
476477
return message;
@@ -480,8 +481,8 @@ class TrackPublishedPayload$Type extends MessageType<TrackPublishedPayload> {
480481
while (reader.pos < end) {
481482
let [fieldNo, wireType] = reader.tag();
482483
switch (fieldNo) {
483-
case /* sfu.TrackInfo remote_track */ 1:
484-
message.remoteTrack = TrackInfo.internalBinaryRead(reader, reader.uint32(), options, message.remoteTrack);
484+
case /* repeated sfu.TrackInfo remote_tracks */ 1:
485+
message.remoteTracks.push(TrackInfo.internalBinaryRead(reader, reader.uint32(), options));
485486
break;
486487
default:
487488
let u = options.readUnknownField;
@@ -495,9 +496,9 @@ class TrackPublishedPayload$Type extends MessageType<TrackPublishedPayload> {
495496
return message;
496497
}
497498
internalBinaryWrite(message: TrackPublishedPayload, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
498-
/* sfu.TrackInfo remote_track = 1; */
499-
if (message.remoteTrack)
500-
TrackInfo.internalBinaryWrite(message.remoteTrack, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
499+
/* repeated sfu.TrackInfo remote_tracks = 1; */
500+
for (let i = 0; i < message.remoteTracks.length; i++)
501+
TrackInfo.internalBinaryWrite(message.remoteTracks[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
501502
let u = options.writeUnknownFields;
502503
if (u !== false)
503504
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -512,12 +513,12 @@ export const TrackPublishedPayload = new TrackPublishedPayload$Type();
512513
class TrackUnpublishedPayload$Type extends MessageType<TrackUnpublishedPayload> {
513514
constructor() {
514515
super("sfu.TrackUnpublishedPayload", [
515-
{ no: 1, name: "remote_track_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
516+
{ no: 1, name: "remote_track_ids", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
516517
]);
517518
}
518519
create(value?: PartialMessage<TrackUnpublishedPayload>): TrackUnpublishedPayload {
519520
const message = globalThis.Object.create((this.messagePrototype!));
520-
message.remoteTrackId = "";
521+
message.remoteTrackIds = [];
521522
if (value !== undefined)
522523
reflectionMergePartial<TrackUnpublishedPayload>(this, message, value);
523524
return message;
@@ -527,8 +528,8 @@ class TrackUnpublishedPayload$Type extends MessageType<TrackUnpublishedPayload>
527528
while (reader.pos < end) {
528529
let [fieldNo, wireType] = reader.tag();
529530
switch (fieldNo) {
530-
case /* string remote_track_id */ 1:
531-
message.remoteTrackId = reader.string();
531+
case /* repeated string remote_track_ids */ 1:
532+
message.remoteTrackIds.push(reader.string());
532533
break;
533534
default:
534535
let u = options.readUnknownField;
@@ -542,9 +543,9 @@ class TrackUnpublishedPayload$Type extends MessageType<TrackUnpublishedPayload>
542543
return message;
543544
}
544545
internalBinaryWrite(message: TrackUnpublishedPayload, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
545-
/* string remote_track_id = 1; */
546-
if (message.remoteTrackId !== "")
547-
writer.tag(1, WireType.LengthDelimited).string(message.remoteTrackId);
546+
/* repeated string remote_track_ids = 1; */
547+
for (let i = 0; i < message.remoteTrackIds.length; i++)
548+
writer.tag(1, WireType.LengthDelimited).string(message.remoteTrackIds[i]);
548549
let u = options.writeUnknownFields;
549550
if (u !== false)
550551
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);

0 commit comments

Comments
 (0)