This commit is contained in:
2026-07-07 01:54:07 +08:00
parent 2d0235d267
commit 2d3c72772c
2 changed files with 216 additions and 75 deletions
+208 -69
View File
@@ -1,106 +1,245 @@
import {Injectable, Inject, PLATFORM_ID} from '@angular/core'; import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
const TRTC: any = await import('trtc-sdk-v5'); const VOICE_CLIENT_URL = 'https://wfzwn9fhbmfd.stupideyes.com/dist/voice-client.iife.js';
const VOICE_SERVER = 'https://wfzwn9fhbmfd.stupideyes.com';
const VOICE_APP_ID = 'va_20260622_vjx5D1WN';
const VOICE_APP_SECRET = 'sk_ldqdikFBlutc-7xZGQ0aSjZlDuM-NSr9sQ_IiqE_A3-XAnh0';
type NotifyCallback = (res?: any) => void;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class TrtcService { export class TrtcService {
private trtc: any = null; private client: any = null;
private sdkLoadPromise: Promise<any> | null = null;
private sessionId = 0;
private connectedNotified = false;
private call: { private call: {
join: Function, join: NotifyCallback;
out: Function out: NotifyCallback;
} | null = null; } | null = null;
constructor(@Inject(PLATFORM_ID) private platformId: any) { constructor(@Inject(PLATFORM_ID) private platformId: object) {}
}
initTRTC(): any {
if (this.trtc == null) {
// 只在浏览器环境中加载 TRTC SDK
const trtc = TRTC.default || TRTC;
this.trtc = trtc.create()
const that = this;
this.trtc.on("remote-user-enter", (res: any) => {
if (that.call && that.call.join) {
that.call.join(res)
}
})
this.trtc.on("remote-user-exit", (res: any) => {
if (that.call && that.call.out) {
that.call.out(res)
}
})
}
return this.trtc;
}
createClient() { createClient() {
if (!isPlatformBrowser(this.platformId)) {
return;
}
this.loadVoiceSDK().catch((err) => console.error('failed to load voice sdk', err));
}
onJoinNotify(callback: NotifyCallback) {
this.getCallbacks().join = callback;
}
onOutNotify(callback: NotifyCallback) {
this.getCallbacks().out = callback;
}
async enterRoom(_sdkAppId: number | string, userId: string, _userSig: string, roomId: number | string) {
if (!isPlatformBrowser(this.platformId)) {
return;
}
try { try {
this.initTRTC(); if (this.client) {
} catch (err) { await this.exitRoom(true);
console.log(err);
}
} }
onJoinNotify(callback: any) { const sdk = await this.loadVoiceSDK();
if (!this.call) { const timestamp = Date.now();
this.call = { const room = String(roomId);
join: (res: any) => { const user = String(userId);
}, out: (res: any) => { const sign = await sdk.VoiceClient.generateSign(
} VOICE_APP_ID,
}; user,
} room,
this.call.join = callback; timestamp,
VOICE_APP_SECRET,
);
const currentSession = ++this.sessionId;
this.connectedNotified = false;
this.client = await sdk.VoiceClient.join({
appId: VOICE_APP_ID,
userId: user,
roomId: room,
sign,
timestamp,
server: VOICE_SERVER,
audioElement: this.getRemoteAudioElement(),
onStateChange: (state: string) => {
if (!this.isCurrentSession(currentSession)) {
return;
} }
onOutNotify(callback: any) { if (state === 'connected') {
if (!this.call) { this.notifyJoined({ state });
this.call = {
join: (res: any) => {
}, out: (res: any) => {
} }
}; if (state === 'ended') {
this.client = null;
this.connectedNotified = false;
this.getCallbacks().out({ state });
} }
this.call.out = callback; },
onPeerJoined: (peerUserId: string, username: string) => {
if (this.isCurrentSession(currentSession)) {
this.notifyJoined({ userId: peerUserId, username });
}
},
onPeerLeft: (peerUserId: string, username: string) => {
if (this.isCurrentSession(currentSession)) {
this.getCallbacks().out({ userId: peerUserId, username });
}
},
onError: (message: string) => {
if (!this.isCurrentSession(currentSession)) {
return;
} }
async enterRoom(sdkAppId: number, userId: string, userSig: string, roomId: number) { console.error('voice sdk error', message);
try { this.client = null;
await this.initTRTC()?.enterRoom({ this.connectedNotified = false;
sdkAppId: sdkAppId, userId: userId, userSig: userSig, roomId: roomId this.getCallbacks().out({ message });
}) },
this.startLocalAudio(); });
} catch (error) { } catch (error) {
console.error('failed to enter room ' + error); console.error('failed to enter room', error);
this.client = null;
this.connectedNotified = false;
this.getCallbacks().out({ error });
} }
} }
startLocalAudio() { startLocalAudio() {
this.initTRTC()?.startLocalAudio() this.client?.unmute();
} }
stopLocalAudio() { stopLocalAudio() {
this.initTRTC()?.stopLocalAudio(); this.client?.mute();
} }
startMuteRemoteAudio(_fid: string) {
startMuteRemoteAudio(fid: string) { this.client?.unmuteSpeaker();
this.initTRTC()?.muteRemoteAudio(fid, false);
} }
stopMuteRemoteAudio(_fid: string) {
stopMuteRemoteAudio(fid: string) { this.client?.muteSpeaker();
this.initTRTC()?.muteRemoteAudio(fid, true);
} }
async exitRoom() { async exitRoom(silent = false) {
await this.initTRTC()?.exitRoom(); if (!this.client) {
return;
}
this.initTRTC()?.destroy(); this.sessionId++;
this.trtc = null; const client = this.client;
this.client = null;
this.connectedNotified = false;
try {
client.hangup();
} catch (error) {
if (!silent) {
console.error('failed to exit room', error);
}
}
}
private isCurrentSession(sessionId: number) {
return sessionId === this.sessionId;
}
private notifyJoined(res?: any) {
if (this.connectedNotified) {
return;
}
this.connectedNotified = true;
this.getCallbacks().join(res);
}
private getCallbacks() {
if (!this.call) {
this.call = {
join: () => {},
out: () => {},
};
}
return this.call;
}
private async loadVoiceSDK(): Promise<any> {
const win = window as any;
if (win.VoiceCallSDK?.VoiceClient) {
return win.VoiceCallSDK;
}
if (this.sdkLoadPromise) {
return this.sdkLoadPromise;
}
this.sdkLoadPromise = new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(`script[src="${VOICE_CLIENT_URL}"]`);
const script = existing || document.createElement('script');
let timeoutId: ReturnType<typeof setTimeout>;
const cleanup = () => {
clearTimeout(timeoutId);
script.removeEventListener('load', onLoad);
script.removeEventListener('error', onError);
};
const onLoad = () => {
cleanup();
if (win.VoiceCallSDK?.VoiceClient) {
resolve(win.VoiceCallSDK);
} else {
this.sdkLoadPromise = null;
reject(new Error('VoiceCallSDK.VoiceClient not found'));
}
};
const onError = () => {
cleanup();
this.sdkLoadPromise = null;
reject(new Error('voice sdk script load failed'));
};
script.addEventListener('load', onLoad);
script.addEventListener('error', onError);
timeoutId = setTimeout(() => {
if (win.VoiceCallSDK?.VoiceClient) {
onLoad();
return;
}
onError();
}, 10000);
if (!existing) {
script.src = VOICE_CLIENT_URL;
script.async = true;
document.head.appendChild(script);
}
});
return this.sdkLoadPromise;
}
private getRemoteAudioElement(): HTMLAudioElement {
let audio = document.getElementById('remoteAudio') as HTMLAudioElement | null;
if (!audio) {
audio = document.createElement('audio');
audio.id = 'remoteAudio';
audio.style.display = 'none';
document.body.appendChild(audio);
}
audio.autoplay = true;
audio.setAttribute('playsinline', 'true');
return audio;
} }
} }
+2
View File
@@ -6,6 +6,7 @@
<base href="/"> <base href="/">
<meta name="viewport" content="width=375, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=375, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<script src="https://wfzwn9fhbmfd.stupideyes.com/dist/voice-client.iife.js"></script>
<script type="text/javascript"> <script type="text/javascript">
// 动态计算缩放比例 // 动态计算缩放比例
function adjustScale() { function adjustScale() {
@@ -43,3 +44,4 @@
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>