Implemented BroadcastChannelService and FileTransferService
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m17s

This commit is contained in:
2026-04-08 08:46:16 +02:00
parent e6798b4be8
commit a9322e3454
10 changed files with 428 additions and 3 deletions

View File

@@ -0,0 +1,28 @@
export interface CreateServerReq {
type: "rtmp",
channelId: string
categoryId: string
networkId: string
}
export interface GetRTMPDataReq {
channelId: string
networkId: string
categoryId: string
}
export interface JoinWebsocketRoomReq {
userid: string
connId: string
channelId: string
networkId: string
categoryId: string
}
export interface StreamRegistry {
streamKey: string
status: "idling" | "broadcasting" | "broadcasting_starting"
type: "rtmp"
streamURL: string
channelId: string
}

View File

@@ -0,0 +1,95 @@
export interface StartNewFileTransferReq {
userid: string
targetUserId: string
metadata: TransferableFileMetadata[]
}
export interface AcceptFileTransferReq {
userid: string
senderId: string
transferId: string
}
export interface DeclineFileTransferReq {
userid: string
senderId: string
transferId: string
}
export interface FileTransferSendOfferRTCReq {
userid: string
peerId: string
transferId: string
offer: string
}
export interface FileTransferSendAnswerRTCReq {
userid: string
peerId: string
transferId: string
answer: string
}
export interface FileTransferSendICERTCReq {
userid: string
peerId: string
transferId: string
candidate: string
}
// Response schemas
export interface StartNewFileTransferResp {
transferId: string
}
// WebSocket payloads
export interface WSNewFileTransferPayload {
from: string
transferId: string
metadata: TransferableFileMetadata[]
}
export interface WSFileTransferAcceptedPayload {
transferId: string
rtcConfig: RTCConfiguration
}
export interface WSFileTransferDeclinedPayload {
transferId: string
}
export interface WSFileTransferRTCOfferPayload {
transferId: string
offer: string
}
export interface WSFileTransferRTCAnswerPayload {
transferId: string
answer: string
}
export interface WSFileTransferRTCIcePayload {
transferId: string
candidate: string
}
// DataChannel payloads
export interface DCStartNewFilePayload {
fileId: string
fileIndex: number
fileName: string
totalChunks: number
}
export interface DCTransferFilePayload {
fileId: string
fileIndex: string
chunk: string
}
// Types
export interface TransferableFileMetadata {
fileId: string
name: string
size: number
}

View File

@@ -0,0 +1,11 @@
import {http, HttpResponse} from "msw";
import {GetRTCAccessResp} from "../../domain/callService.schema";
import {StreamRegistry} from "../../domain/broadcastChannelService.schema";
export const brcChanHandlers = [
http.get('*/network/channel/rtmpData', () => {
return HttpResponse.json(<StreamRegistry>{
status: "broadcasting_starting"
})
}),
]

View File

@@ -6,6 +6,7 @@ import {fileUploadHandlers} from "./handlers/fUpl.http";
import {chatHandlers} from "./handlers/chat.http";
import {dmHandlers} from "./handlers/dm.http";
import {userHandler} from "./handlers/user.http";
import {brcChanHandlers} from "./handlers/brcChan.http";
export const allHandlers = [
...authHandlers,
@@ -15,5 +16,6 @@ export const allHandlers = [
...fileUploadHandlers,
...chatHandlers,
...dmHandlers,
...userHandler
...userHandler,
...brcChanHandlers
]

View File

@@ -0,0 +1,11 @@
import {describe, expect, it} from "vitest";
import {BroadcastChannelService} from "./broadcastChannelService";
describe("BroadcastChannelService", () => {
const service = new BroadcastChannelService("", "", "", "", "", () => {})
it("should get stream registry data", async () => {
const data = await service.getData()
expect(data.status).toBe("broadcasting_starting")
})
})

View File

@@ -0,0 +1,93 @@
import {AxiosInstance, isAxiosError} from "axios";
import {getClient} from "../core/http";
import {MessageListener} from "../domain/websocket.schema";
import {WebSocketHandler} from "../core/webSocketHandler";
import {AcceptInviteReq} from "../domain/networkService.schema";
import {GenericErrorBody} from "../domain/http.schema";
import {
CreateServerReq,
GetRTMPDataReq,
JoinWebsocketRoomReq,
StreamRegistry
} from "../domain/broadcastChannelService.schema";
export class BroadcastChannelService {
userid: string
channelId: string
networkId: string
categoryId: string
client: AxiosInstance;
constructor(token: string, userid: string, networkId: string, categoryId: string, channelId: string, wsMessageListener: MessageListener) {
this.userid = userid;
this.channelId = channelId;
this.categoryId = categoryId;
this.networkId = networkId;
this.client = getClient(false).create({
headers: {
"Authorization": token,
"X-WS-ID": WebSocketHandler.getInstance().connId
}
})
WebSocketHandler.getInstance().registerService({
identifier: channelId,
onNewConnId: this.onNewConnId,
onNewMessage: wsMessageListener,
})
}
private onNewConnId(newConnId: string) {
console.log("NetworkService: New connection id")
this.client.defaults.headers["X-WS-ID"] = newConnId;
}
async getData(): Promise<StreamRegistry> {
try {
const resp = await this.client.get<StreamRegistry>(`network/channel/rtmpData?userid=${this.userid}&channelId=${this.channelId}&networkId=${this.networkId}&categoryId=${this.categoryId}`);
return resp.data
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async createServer(): Promise<StreamRegistry> {
try {
const resp = await this.client.post<StreamRegistry>("network/channel/createServer", <CreateServerReq>{
userid: this.userid,
channelId: this.channelId,
networkId: this.networkId,
type: "rtmp",
categoryId: this.categoryId,
});
return resp.data
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async joinWebSocketRoom(): Promise<void> {
try {
await this.client.post("v2/network/channel/joinWebSocketRoom", <JoinWebsocketRoomReq>{
userid: this.userid,
channelId: this.channelId,
networkId: this.networkId,
connId: WebSocketHandler.getInstance().connId,
categoryId: this.categoryId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -0,0 +1,156 @@
import {DatabaseAPI} from "../storage/database";
import {AxiosInstance, isAxiosError} from "axios";
import {MessageListener} from "../domain/websocket.schema";
import {getClient} from "../core/http";
import {WebSocketHandler} from "../core/webSocketHandler";
import {CreateNetworkReq, Network} from "../domain/networkService.schema";
import {GenericErrorBody} from "../domain/http.schema";
import {
AcceptFileTransferReq, DeclineFileTransferReq, FileTransferSendAnswerRTCReq,
FileTransferSendICERTCReq, FileTransferSendOfferRTCReq,
StartNewFileTransferReq,
StartNewFileTransferResp,
TransferableFileMetadata
} from "../domain/fileTransferService.schema";
export class FileTransferService {
userid: string;
peerId: string;
transferId: string = "";
client: AxiosInstance
constructor(userid: string, token: string, peerId: string) {
this.userid = userid;
this.peerId = peerId;
this.client = getClient(false).create({
headers: {
"Authorization": token,
}
})
}
/**
* Starts a new file transfer with the specified user. The new transferId will be saved automatically and not required to be provided later.
* @param transferableFiles
*/
async startNew(transferableFiles: TransferableFileMetadata[]): Promise<string> {
try {
const resp = await this.client.post<StartNewFileTransferResp>("v2/chat/dm/startNewFileTransfer", <StartNewFileTransferReq>{
userid: this.userid,
targetUserId: this.peerId,
metadata: transferableFiles
});
this.transferId = resp.data.transferId
return resp.data.transferId
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Accepts the file transfer with the specified transferId. The transferId will be saved automatically and not required to be provided later.
* @param transferId
*/
async accept(transferId: string): Promise<RTCConfiguration> {
try {
const resp = await this.client.post<RTCConfiguration>("v2/chat/dm/acceptFileTransfer", <AcceptFileTransferReq>{
userid: this.userid,
senderId: this.peerId,
transferId: transferId
});
this.transferId = transferId
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Declines the file transfer with the specified transferId.
* @param transferId
*/
async decline(transferId: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/declineFileTransfer", <DeclineFileTransferReq>{
userid: this.userid,
senderId: this.peerId,
transferId: transferId
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC offer to the specified user.
* @param offer
*/
async sendRtcOffer(offer: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcOfferFileTransfer", <FileTransferSendOfferRTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
offer: offer,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC answer to the specified user.
* @param answer
*/
async sendRtcAnswer(answer: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcAnswerFileTransfer", <FileTransferSendAnswerRTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
answer: answer,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC ICE candidate to the specified user.
* @param candidate
*/
async sendRtcICE(candidate: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcICEFileTransfer", <FileTransferSendICERTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
candidate: candidate,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -0,0 +1,29 @@
import {describe, expect, it} from "vitest";
import {BroadcastChannelService} from "../src/services/broadcastChannelService";
const BRC_CHAN_SERVICE_TESTING_NETWORK_ID = "000000000000000000000000"
const BRC_CHAN_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const BRC_CHAN_SERVICE_TESTING_CHANNEL_ID = "333333333333333333333333"
const BRC_CHAN_SERVICE_TESTING_TOKEN = "testingToken"
const BRC_CHAN_SERVICE_TESTING_CATEGORY_ID = "111111111111111111111111"
describe("BroadcastChannelService Integration Testing", () => {
const service = new BroadcastChannelService(
BRC_CHAN_SERVICE_TESTING_TOKEN,
BRC_CHAN_SERVICE_TESTING_USER_ID,
BRC_CHAN_SERVICE_TESTING_NETWORK_ID,
BRC_CHAN_SERVICE_TESTING_CATEGORY_ID,
BRC_CHAN_SERVICE_TESTING_CHANNEL_ID,
(action, data) => {}
)
it('should create a new server and fetch it', async () => {
await service.createServer()
const registry = await service.getData()
expect(registry.status).toBe("idling")
});
it('should join ws room', async () => {
await service.joinWebSocketRoom()
});
})

View File

@@ -74,7 +74,7 @@ describe("NetworkService Integration Testing", () => {
});
it('should create rank', async () => {
const rankName = faker.internet.displayName()
const rankName = faker.internet.displayName().substring(0, 10)
const rank = await service.createRank(rankName, <RGB>{r: 0, g: 0, b: 0}, null)
expect(rank.name).toBe(rankName)
});

View File

@@ -24,7 +24,7 @@ describe("PictureService Integration Test", () => {
});
it('should create an album', async () => {
const albumName = faker.internet.displayName()
const albumName = faker.internet.displayName().substring(0, 10)
await service.createAlbum(albumName)
const uploads = await service.get()
expect(uploads.pictures[1].name).toBe(albumName)