Преглед изворни кода

feat: 新增基于阿里云RTC的实时音视频通信功能

实现RTC频道加入/离开、本地预览、远端用户视频显示等核心功能
添加房间管理服务和相关路由
更新SDK依赖配置
yourname пре 7 месеци
родитељ
комит
29708b4064
9 измењених фајлова са 3053 додато и 1961 уклоњено
  1. 13 0
      client/mobile/mobile_app.tsx
  2. 434 0
      client/mobile/pages_live.tsx
  3. 262 0
      client/mobile/pages_rtc.tsx
  4. 1515 0
      client/share/alivc-im.iife.d.ts
  5. 6 0
      deno.json
  6. 387 1961
      deno.lock
  7. 2 0
      server/app.tsx
  8. 263 0
      server/room_management.ts
  9. 171 0
      server/routes_live.ts

+ 13 - 0
client/mobile/mobile_app.tsx

@@ -19,6 +19,9 @@ import LoginPage from './pages_login.tsx';
 import { GlobalConfig } from "../share/types.ts";
 import { ExclamationTriangleIcon, HomeIcon, BellIcon, UserIcon } from '@heroicons/react/24/outline';
 import { NotificationsPage } from './pages_messages.tsx';
+import { LivePage } from './pages_live.tsx';
+import { RTCPage } from './pages_rtc.tsx';
+
 // 设置中文语言
 dayjs.locale('zh-cn');
 
@@ -249,6 +252,16 @@ const App = () => {
       element: <Navigate to="/mobile" replace />,
       errorElement: <ErrorPage />
     },
+    {
+      path: '/mobile/live',
+      element: <LivePage />,
+      errorElement: <ErrorPage />
+    },
+    {
+      path: '/mobile/rtc',
+      element: <RTCPage />,
+      errorElement: <ErrorPage />
+    },
     {
       path: '/mobile/login',
       element: <LoginPage />,

+ 434 - 0
client/mobile/pages_live.tsx

@@ -0,0 +1,434 @@
+import React, { useState, useEffect } from 'react';
+// import 'https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js'
+
+// 从 SDK 中提取需要的类型
+type ImEngine = InstanceType<typeof AliVCInteraction.ImEngine>;
+type ImGroupManager = AliVCInteraction.AliVCIMGroupManager;
+type ImMessageManager = AliVCInteraction.AliVCIMMessageManager;
+type ImLogLevel = AliVCInteraction.ImLogLevel;
+type ImMessageLevel = AliVCInteraction.ImMessageLevel;
+
+interface ImUser {
+  userId: string;
+  userExtension?: string;
+}
+
+interface ImGroupMessage {
+  groupId: string;
+  type: number;
+  data: string;
+  sender?: ImUser;
+  timestamp?: number;
+}
+
+interface ImGroupMemberChangeEvent {
+  groupId: string;
+  memberCount: number;
+  joinUsers: ImUser[];
+  leaveUsers: ImUser[];
+}
+
+interface ImGroupMuteChangeEvent {
+  groupId: string;
+  status: AliVCInteraction.ImGroupMuteStatus;
+}
+
+interface ImGroupInfoChangeEvent {
+  groupId: string;
+  info: AliVCInteraction.ImGroupInfoStatus;
+}
+
+interface ImError {
+  code: number;
+  msg: string;
+  message?: string;
+}
+
+const { ImEngine: ImEngineClass, ImLogLevel, ImMessageLevel } = window.AliVCInteraction;
+
+// 请从控制台复制对应的值填入下面 AppId、 AppKey、AppSign 变量当中
+// 注意:这里仅作为本地快速体验使用,实际开发请勿在前端泄露 AppKey
+const AppId = '4c2ab5e1b1b0';
+const AppKey = '314bb5eee5b623549e8a41574ba3ff32';
+const AppSign = 'H4sIAAAAAAAE/wCQAG//zguHB+lYCilkv7diSkk4GmcvLuds+InRu9vFOFebMwm/jEgsK5bBT85Z0owObMxG58uXHyPFlPEBEDQm9FswNJ+KmX0VDYkcfdPPWkafA6Hc0B6F+p5De9yJfPEfHzwo/DHMaygbHfLmBgUtmKveq421sJr/gNBz9D04Ewsg39us+ao0NegzLt7xtXvFXXXJAAAA//8BAAD//yoav6aQAAAA';
+
+export const LivePage = () => {
+    const [userId, setUserId] = useState<string>('');
+    const [groupId, setGroupId] = useState<string>('');
+    const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
+    const [isJoinedGroup, setIsJoinedGroup] = useState<boolean>(false);
+    const [msgText, setMsgText] = useState<string>('');
+    const [messageList, setMessageList] = useState<string[]>([]);
+    const [errorMessage, setErrorMessage] = useState<string>('');
+
+    const engine: ImEngine = ImEngineClass.createEngine();
+    const [groupManager, setGroupManager] = useState<ImGroupManager | null>(null);
+    const [messageManager, setMessageManager] = useState<ImMessageManager | null>(null);
+    const [joinedGroupId, setJoinedGroupId] = useState<string | null>(null);
+
+    const sha256 = async (message: string): Promise<string> => {
+        const encoder = new TextEncoder();
+        const data = encoder.encode(message);
+        const result = await crypto.subtle.digest('SHA-256', data).then((buffer) => {
+            let hash = Array.prototype.map.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)).join('');
+            return hash;
+        });
+        return result;
+    };
+
+    const getLoginAuth = async (userId: string, role: string): Promise<AliVCInteraction.ImAuth> => {
+        const nonce = 'AK_4';
+        const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
+        const pendingShaStr = `${AppId}${AppKey}${userId}${nonce}${timestamp}${role}`;
+        const appToken = await sha256(pendingShaStr);
+        return {
+            nonce,
+            timestamp,
+            token: appToken,
+            role,
+        };
+    };
+
+    const showMessage = (text: string): void => {
+        setMessageList([...messageList, text]);
+    };
+
+    const listenEngineEvents = (): void => {
+        engine.on('connecting', () => {
+            console.log('connecting');
+        });
+        engine.on('connectfailed', (err) => {
+            console.log(`connect failed: ${err.message}`);
+        });
+        engine.on('connectsuccess', () => {
+            console.log('connect success');
+        });
+        engine.on('disconnect', (code: number) => {
+            console.log(`disconnect: ${code}`);
+        });
+        engine.on('tokenexpired', async (cb: (error: null, auth: AliVCInteraction.ImAuth) => void) => {
+            console.log('token expired');
+            // 这里需要更新为获取新的登录信息的代码
+            // const auth = await getLoginAuth(userId, role);
+            // cb(null, auth);
+        });
+    };
+
+    const listenGroupEvents = (): void => {
+        if (!groupManager) {
+            return;
+        }
+        groupManager.on('exit', (groupId: string, reason: number) => {
+            showMessage(`group ${groupId} close, reason: ${reason}`);
+        });
+        groupManager.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => {
+            showMessage(`group ${groupId} member change, memberCount: ${memberCount}, joinUsers: ${joinUsers.map(u => u.userId).join(',')}, leaveUsers: ${leaveUsers.map(u => u.userId).join('')}`);
+        });
+        groupManager.on('mutechange', (groupId: string, status: AliVCInteraction.ImGroupMuteStatus) => {
+            showMessage(`group ${groupId} mute change`);
+        });
+        groupManager.on('infochange', (groupId: string, info: AliVCInteraction.ImGroupInfoStatus) => {
+            showMessage(`group ${groupId} info change`);
+        });
+    };
+
+    const listenMessageEvents = (): void => {
+        if (!messageManager) {
+            return;
+        }
+        messageManager.on('recvgroupmessage', (msg: AliVCInteraction.ImMessage, groupId: string) => {
+            console.log('recvgroupmessage', msg, groupId);
+            showMessage(`receive group: ${groupId}, type: ${msg.type}, data: ${msg.data}`);
+        });
+    };
+
+    const login = async (userId: string): Promise<void> => {
+        try {
+            await engine.init({
+                deviceId: 'xxxx',
+                appId: AppId,
+                appSign: AppSign,
+                logLevel: ImLogLevel.ERROR,
+            });
+            listenEngineEvents();
+            const role = 'admin';
+            const authData = await getLoginAuth(userId, role);
+            await engine.login({
+                user: {
+                    userId,
+                    userExtension: '{}',
+                },
+                userAuth: {
+                    timestamp: authData.timestamp,
+                    nonce: authData.nonce,
+                    role: authData.role,
+                    token: authData.token,
+                },
+            });
+            const gm = engine.getGroupManager();
+            const mm = engine.getMessageManager();
+            setGroupManager(gm || null);
+            setMessageManager(mm || null);
+            setIsLoggedIn(true);
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`初始化、登录失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const logout = async (): Promise<void> => {
+        try {
+            await engine.logout();
+            engine.unInit();
+            setGroupManager(null);
+            setMessageManager(null);
+            setJoinedGroupId(null);
+            setIsLoggedIn(false);
+            setIsJoinedGroup(false);
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`登出失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const joinGroup = async (groupId: string): Promise<void> => {
+        try {
+            if (!groupManager) {
+                return;
+            }
+            await groupManager.joinGroup(groupId);
+            setJoinedGroupId(groupId);
+            listenGroupEvents();
+            listenMessageEvents();
+            setIsJoinedGroup(true);
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`加入群组失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const leaveGroup = async (): Promise<void> => {
+        try {
+            if (!groupManager || !joinedGroupId) {
+                return;
+            }
+            await groupManager.leaveGroup(joinedGroupId);
+            groupManager.removeAllListeners();
+            messageManager?.removeAllListeners();
+            setJoinedGroupId(null);
+            setIsJoinedGroup(false);
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`离开群组失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const createGroup = async (): Promise<void> => {
+        try {
+            if (!groupManager) {
+                return;
+            }
+            const res = await groupManager.createGroup({
+                groupId,
+                groupName: 'xxx',
+                groupMeta: 'xxx',
+            });
+            console.log('创建群组成功', res);
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`创建群组失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const sendMessage = async (): Promise<void> => {
+        try {
+            if (!messageManager || !joinedGroupId) {
+                return;
+            }
+            const res = await messageManager.sendGroupMessage({
+                groupId: joinedGroupId,
+                data: msgText,
+                type: 88888,
+                skipAudit: false,
+                skipMuteCheck: false,
+                level: ImMessageLevel.NORMAL,
+                noStorage: true,
+                repeatCount: 1,
+            });
+            console.log('群消息发送成功', res);
+            setMsgText('');
+            setErrorMessage('');
+        } catch (err: any) {
+            setErrorMessage(`群消息发送失败: ${err.code} ${err.msg}`);
+        }
+    };
+
+    const clearMessages = (): void => {
+        setMessageList([]);
+    };
+
+    useEffect(() => {
+        return () => {
+            if (groupManager) {
+                groupManager.removeAllListeners();
+            }
+            if (messageManager) {
+                messageManager.removeAllListeners();
+            }
+            if (engine) {
+                engine.removeAllListeners();
+            }
+        };
+    }, []);
+
+    return (
+        <div className="container mx-auto p-4">
+            <h1 className="text-2xl font-bold mb-4">互动消息 quick start</h1>
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div>
+                    <form>
+                        <div className="mb-2">
+                            <label htmlFor="userId" className="block text-sm font-medium text-gray-700">用户ID</label>
+                            <input
+                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+                                id="userId"
+                                placeholder="请输入英文字母或数字"
+                                value={userId}
+                                onChange={(e) => setUserId(e.target.value)}
+                            />
+                        </div>
+                        <div className="mb-2">
+                            <label htmlFor="groupId" className="block text-sm font-medium text-gray-700">群组ID</label>
+                            <input
+                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+                                id="groupId"
+                                placeholder="加入群组前请确认是否已存在,不存在先创建"
+                                value={groupId}
+                                onChange={(e) => setGroupId(e.target.value)}
+                            />
+                        </div>
+                        <div className="mb-2 flex space-x-2">
+                            <button
+                                id="loginBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={isLoggedIn}
+                                onClick={() => login(userId)}
+                            >
+                                登录
+                            </button>
+                            <button
+                                id="joinBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn && isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={isLoggedIn && isJoinedGroup}
+                                onClick={() => joinGroup(groupId)}
+                            >
+                                加入群组
+                            </button>
+                            <button
+                                id="createBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${!isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={!isLoggedIn}
+                                onClick={() => createGroup()}
+                            >
+                                创建群组
+                            </button>
+                            <button
+                                id="leaveBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={!isLoggedIn ||!isJoinedGroup}
+                                onClick={() => leaveGroup()}
+                            >
+                                离开群组
+                            </button>
+                            <button
+                                id="logoutBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={!isLoggedIn}
+                                onClick={() => logout()}
+                            >
+                                登出
+                            </button>
+                        </div>
+                        <p className="mb-2 text-sm text-gray-600">假如群组ID已存在,可以一键登录+加入群组</p>
+                        <div className="mb-2 flex space-x-2">
+                            <button
+                                id="oneLoginBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn && isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={isLoggedIn && isJoinedGroup}
+                                onClick={() => {
+                                    if (userId && groupId) {
+                                        login(userId).then(() => {
+                                            joinGroup(groupId);
+                                        });
+                                    }
+                                }}
+                            >
+                                一键登录+加入群组
+                            </button>
+                            <button
+                                id="oneLogoutBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={!isLoggedIn ||!isJoinedGroup}
+                                onClick={() => {
+                                    leaveGroup().then(() => {
+                                        logout();
+                                    });
+                                }}
+                            >
+                                一键离开群组+登出
+                            </button>
+                        </div>
+                    </form>
+                    <form className="mt-4">
+                        <div className="mb-2">
+                            <label htmlFor="msgText" className="block text-sm font-medium text-gray-700">消息</label>
+                            <input
+                                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+                                id="msgText"
+                                value={msgText}
+                                onChange={(e) => setMsgText(e.target.value)}
+                            />
+                        </div>
+                        <div className="mb-2">
+                            <button
+                                id="sendBtn"
+                                type="button"
+                                className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
+                                disabled={!isLoggedIn ||!isJoinedGroup}
+                                onClick={() => sendMessage()}
+                            >
+                                发送
+                            </button>
+                        </div>
+                    </form>
+                </div>
+                <div>
+                    <h5 className="flex justify-between items-center text-lg font-medium mb-2">
+                        消息展示
+                        <button
+                            id="clearBtn"
+                            type="button"
+                            className="px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
+                            onClick={clearMessages}
+                        >
+                            清空
+                        </button>
+                    </h5>
+                    <div id="msgList" className="mt-4 space-y-2">
+                        {messageList.map((msg, index) => (
+                            <div key={index} className="bg-gray-100 p-2 rounded-md">{msg}</div>
+                        ))}
+                    </div>
+                </div>
+            </div>
+            {errorMessage && <div className="mt-2 text-red-500">{errorMessage}</div>}
+        </div>
+    );
+};

+ 262 - 0
client/mobile/pages_rtc.tsx

@@ -0,0 +1,262 @@
+import React, { useState, useEffect, useRef } from 'react';
+import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack,AliRtcSdkChannelProfile } from 'aliyun-rtc-sdk';
+import { ToastContainer, toast } from 'react-toastify';
+
+// 辅助函数
+function hex(buffer: ArrayBuffer): string {
+    const hexCodes = [];
+    const view = new DataView(buffer);
+    for (let i = 0; i < view.byteLength; i += 4) {
+        const value = view.getUint32(i);
+        const stringValue = value.toString(16);
+        const padding = '00000000';
+        const paddedValue = (padding + stringValue).slice(-padding.length);
+        hexCodes.push(paddedValue);
+    }
+    return hexCodes.join('');
+}
+
+async function generateToken(
+    appId: string,
+    appKey: string,
+    channelId: string,
+    userId: string,
+    timestamp: number
+): Promise<string> {
+    const encoder = new TextEncoder();
+    const data = encoder.encode(`${appId}${appKey}${channelId}${userId}${timestamp}`);
+    const hash = await crypto.subtle.digest('SHA-256', data);
+    return hex(hash);
+}
+
+function showToast(type: 'info' | 'success' | 'error', message: string): void {
+    switch(type) {
+        case 'info':
+            toast.info(message);
+            break;
+        case 'success':
+            toast.success(message);
+            break;
+        case 'error':
+            toast.error(message);
+            break;
+    }
+}
+
+const appId = 'a5842c2a-d94a-43be-81de-1fdb712476e1';
+const appKey = 'b71d65f4f84c450f6f058f4ad507bd42';
+
+export const RTCPage = () => {
+    const [channelId, setChannelId] = useState<string>('');
+    const [userId, setUserId] = useState<string>('');
+    const [isJoined, setIsJoined] = useState<boolean>(false);
+    const aliRtcEngine = useRef<AliRtcEngine | null>(null);
+    const remoteVideoElMap = useRef<Record<string, HTMLVideoElement>>({});
+    const remoteVideoContainer = useRef<HTMLDivElement>(null);
+
+    function removeRemoteVideo(userId: string, type: 'camera' | 'screen' = 'camera') {
+        const vid = `${type}_${userId}`;
+        const el = remoteVideoElMap.current[vid];
+        if (el) {
+            aliRtcEngine.current!.setRemoteViewConfig(null, userId, type === 'camera' ? AliRtcVideoTrack.AliRtcVideoTrackCamera : AliRtcVideoTrack.AliRtcVideoTrackScreen);
+            el.pause();
+            remoteVideoContainer.current?.removeChild(el);
+            delete remoteVideoElMap.current[vid];
+        }
+    }
+
+    function listenEvents() {
+        if (!aliRtcEngine.current) {
+            return;
+        }
+        aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string, elapsed: number) => {
+            console.log(`用户 ${userId} 加入频道,耗时 ${elapsed} 秒`);
+            showToast('info', `用户 ${userId} 上线`);
+        });
+
+        aliRtcEngine.current.on('remoteUserOffLineNotify', (userId, reason) => {
+            console.log(`用户 ${userId} 离开频道,原因码: ${reason}`);
+            showToast('info', `用户 ${userId} 下线`);
+            removeRemoteVideo(userId, 'camera');
+            removeRemoteVideo(userId, 'screen');
+        });
+
+        aliRtcEngine.current.on('bye', (code) => {
+            console.log(`bye, code=${code}`);
+            showToast('info', `您已离开频道,原因码: ${code}`);
+        });
+
+        aliRtcEngine.current.on('videoSubscribeStateChanged', (
+            userId: string,
+            oldState: AliRtcSubscribeState,
+            newState: AliRtcSubscribeState,
+            interval: number,
+            channelId: string
+        ) => {
+            console.log(`频道 ${channelId} 远端用户 ${userId} 订阅状态由 ${oldState} 变为 ${newState}`);
+            const vid = `camera_${userId}`;
+            if (newState === 3) {
+                const video = document.createElement('video');
+                video.autoplay = true;
+                video.className = 'w-80 h-45 mr-2 mb-2 bg-black';
+                remoteVideoElMap.current[vid] = video;
+                remoteVideoContainer.current?.appendChild(video);
+                aliRtcEngine.current!.setRemoteViewConfig(video, userId, AliRtcVideoTrack.AliRtcVideoTrackCamera);
+            } else if (newState === 1) {
+                removeRemoteVideo(userId, 'camera');
+            }
+        });
+
+        aliRtcEngine.current.on('screenShareSubscribeStateChanged', (
+            userId: string,
+            oldState: AliRtcSubscribeState,
+            newState: AliRtcSubscribeState,
+            interval: number,
+            channelId: string
+        ) => {
+            console.log(`频道 ${channelId} 远端用户 ${userId} 屏幕流的订阅状态由 ${oldState} 变为 ${newState}`);
+            const vid = `screen_${userId}`;
+            if (newState === 3) {
+                const video = document.createElement('video');
+                video.autoplay = true;
+                video.className = 'w-80 h-45 mr-2 mb-2 bg-black';
+                remoteVideoElMap.current[vid] = video;
+                remoteVideoContainer.current?.appendChild(video);
+                aliRtcEngine.current!.setRemoteViewConfig(video, userId, AliRtcVideoTrack.AliRtcVideoTrackScreen);
+            } else if (newState === 1) {
+                removeRemoteVideo(userId, 'screen');
+            }
+        });
+    }
+
+    const handleLoginSubmit = async (e: React.FormEvent) => {
+        e.preventDefault();
+        const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
+
+        if (!channelId || !userId) {
+            showToast('error', '数据不完整');
+            return;
+        }
+
+        const engine = AliRtcEngine.getInstance();
+        aliRtcEngine.current = engine;
+        listenEvents();
+
+        try {
+            const token = await generateToken(appId, appKey, channelId, userId, timestamp);
+            aliRtcEngine.current!.setChannelProfile(AliRtcSdkChannelProfile.AliRtcSdkCommunication);
+            await aliRtcEngine.current.joinChannel(
+                {
+                    channelId,
+                    userId,
+                    appId,
+                    token,
+                    timestamp,
+                },
+                userId
+            );
+            showToast('success', '加入频道成功');
+            setIsJoined(true);
+            aliRtcEngine.current!.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
+        } catch (error) {
+            console.log('加入频道失败', error);
+            showToast('error', '加入频道失败');
+        }
+    };
+
+    const handleLeaveClick = async () => {
+        Object.keys(remoteVideoElMap.current).forEach(vid => {
+            const arr = vid.split('_');
+            removeRemoteVideo(arr[1], arr[0] as 'screen' | 'camera');
+        });
+        if (aliRtcEngine.current) {
+            await aliRtcEngine.current.stopPreview();
+            await aliRtcEngine.current.leaveChannel();
+            aliRtcEngine.current.destroy();
+            aliRtcEngine.current = null;
+        }
+        setIsJoined(false);
+        showToast('info', '已离开频道');
+    };
+
+    useEffect(() => {
+        AliRtcEngine.setLogLevel(0);
+    }, []);
+
+    return (
+        <div className="container p-2">
+            <h1 className="text-2xl font-bold mb-4">aliyun-rtc-sdk 快速开始</h1>
+
+            <ToastContainer
+                position="top-right"
+                autoClose={5000}
+                hideProgressBar={false}
+                newestOnTop={false}
+                closeOnClick
+                rtl={false}
+                pauseOnFocusLoss
+                draggable
+                pauseOnHover
+            />
+
+            <div className="flex flex-wrap -mx-2 mt-6">
+                <div className="w-full md:w-1/2 px-2 mb-4">
+                    <form id="loginForm" onSubmit={handleLoginSubmit}>
+                        <div className="mb-2">
+                            <label htmlFor="channelId" className="block text-gray-700 text-sm font-bold mb-2">频道号</label>
+                            <input
+                                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
+                                id="channelId"
+                                type="text"
+                                value={channelId}
+                                onChange={(e) => setChannelId(e.target.value)}
+                            />
+                        </div>
+                        <div className="mb-2">
+                            <label htmlFor="userId" className="block text-gray-700 text-sm font-bold mb-2">用户ID</label>
+                            <input
+                                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
+                                id="userId"
+                                type="text"
+                                value={userId}
+                                onChange={(e) => setUserId(e.target.value)}
+                            />
+                        </div>
+                        <button
+                            id="joinBtn"
+                            type="submit"
+                            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-2"
+                            disabled={isJoined}
+                        >
+                            加入频道
+                        </button>
+                        <button
+                            id="leaveBtn"
+                            type="button"
+                            className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-2"
+                            disabled={!isJoined}
+                            onClick={handleLeaveClick}
+                        >
+                            离开频道
+                        </button>
+                    </form>
+
+                    <div className="mt-6">
+                        <h4 className="text-lg font-bold mb-2">本地预览</h4>
+                        <video
+                            id="localPreviewer"
+                            muted
+                            className="w-80 h-45 mr-2 mb-2 bg-black"
+                        ></video>
+                    </div>
+                </div>
+                <div className="w-full md:w-1/2 px-2">
+                    <h4 className="text-lg font-bold mb-2">远端用户</h4>
+                    <div id="remoteVideoContainer" ref={remoteVideoContainer}></div>
+                </div>
+            </div>
+        </div>
+    );
+};
+
+    

+ 1515 - 0
client/share/alivc-im.iife.d.ts

@@ -0,0 +1,1515 @@
+declare namespace AliVCInteraction {
+  class AliVCIMAttachmentManager {
+    private static instance;
+    private wasmIns;
+    private attachmentManager;
+    private uploader;
+    constructor(wasmIns: any, wasmInterface: any);
+    static getInstance(wasmIns: any, wasmInterface: any): AliVCIMAttachmentManager;
+    getAttachmentReq(attachmentReq: ImAttachmentReq): Promise<any>;
+    /**
+     * 上传附件
+     * @param {string} reqId
+     * @param {ImAttachmentReq} attachmentReq
+     * @returns {ImAttachmentRes}
+     */
+    uploadAttachment(reqId: string, attachmentReq: ImAttachmentReq): Promise<ImAttachmentRes>;
+    /**
+     * 取消上传附件
+     * @param {string} reqId
+     * @returns
+     */
+    cancelAttachmentUpload(reqId: string): Promise<void>;
+    /**
+     * 删除已上传附件
+     * @param {ImAttachmentRes} attachment
+     * @returns
+     */
+    deleteAttachment(attachment: ImAttachmentRes): Promise<void>;
+    destroy(): void;
+}
+
+class AliVCIMGroupManager extends EventEmitter<ImGroupListener> {
+    private wasmIns;
+    private wasmGroupManager;
+    private groupListener;
+    constructor(wasmIns: any, wasmInterface: any);
+    addGroupListener(): void;
+    removeGroupListener(): void;
+    destroy(): void;
+    /**
+     * 创建群组,限管理员才能操作
+     * @param {ImCreateGroupReq} req
+     * @returns {Promise<ImCreateGroupRsp>}
+     */
+    createGroup(req: ImCreateGroupReq): Promise<ImCreateGroupRsp>;
+    /**
+     * 查询群组信息
+     * @param {string | ImQueryGroupReq} groupIdOrReq
+     * @returns {Promise<ImGroupInfo>}
+     */
+    queryGroup(groupIdOrReq: string | ImQueryGroupReq): Promise<ImGroupInfo>;
+    /**
+     * 关闭群组,限管理员才能操作
+     * @param {string | ImCloseGroupReq} groupIdOrReq
+     * @returns {Promise<void>}
+     */
+    closeGroup(groupIdOrReq: string | ImCloseGroupReq): Promise<void>;
+    /**
+     * 加入群组
+     * @param {string | ImJoinGroupReq} groupIdOrReq
+     * @returns {Promise<ImGroupInfo>}
+     */
+    joinGroup(groupIdOrReq: string | ImJoinGroupReq): Promise<ImGroupInfo>;
+    /**
+     * 离开群组
+     * @param {string | ImLeaveGroupReq} groupIdOrReq
+     * @returns {Promise<void>}
+     */
+    leaveGroup(groupIdOrReq: string | ImLeaveGroupReq): Promise<void>;
+    /**
+     * 修改群组信息
+     * @param {ImModifyGroupReq} req
+     * @returns {Promise<void>}
+     */
+    modifyGroup(req: ImModifyGroupReq): Promise<void>;
+    /**
+     * 查询最近组成员
+     * @param {string | ImListRecentGroupUserReq} groupIdOrReq
+     * @returns {Promise<ImListRecentGroupUserRsp>}
+     */
+    listRecentGroupUser(groupIdOrReq: string | ImListRecentGroupUserReq): Promise<ImListRecentGroupUserRsp>;
+    /**
+     * 查询群组成员,限管理员才能操作
+     * @param {string | ImListGroupUserReq} groupIdOrReq
+     * @returns {Promise<ImListGroupUserRsp>}
+     */
+    listGroupUser(groupIdOrReq: string | ImListGroupUserReq): Promise<ImListGroupUserRsp>;
+    /**
+     * 全体禁言,限管理员才能操作
+     * @param {string | ImMuteAllReq} groupIdOrReq
+     * @returns {Promise<void>}
+     */
+    muteAll(groupIdOrReq: string | ImMuteAllReq): Promise<void>;
+    /**
+     * 取消全体禁言,限管理员才能操作
+     * @param {string | ImCancelMuteAllReq} groupIdOrReq
+     * @returns {Promise<void>}
+     */
+    cancelMuteAll(groupIdOrReq: string | ImCancelMuteAllReq): Promise<void>;
+    /**
+     * 禁言指定用户,限管理员才能操作
+     * @param {ImMuteUserReq} req
+     * @returns {Promise<void>}
+     */
+    muteUser(req: ImMuteUserReq): Promise<void>;
+    /**
+     * 取消禁言指定用户,限管理员才能操作
+     * @param {ImCancelMuteUserReq} req
+     * @returns {Promise<void>}
+     */
+    cancelMuteUser(req: ImCancelMuteUserReq): Promise<void>;
+    /**
+     * 查询禁言用户列表,限管理员才能操作
+     * @param {string | ImListMuteUsersReq} groupIdOrReq
+     * @returns {Promise<ImListMuteUsersRsp>}
+     */
+    listMuteUsers(groupIdOrReq: string | ImListMuteUsersReq): Promise<ImListMuteUsersRsp>;
+}
+
+class AliVCIMMessageManager extends EventEmitter<ImMessageListener> {
+    private wasmIns;
+    private wasmMessageManager;
+    private messageListener;
+    private streamMessageManager;
+    constructor(wasmIns: any, wasmInterface: any);
+    addMessageListener(): void;
+    removeMessageListener(): void;
+    destroy(): void;
+    /**
+     * 发送单聊普通消息
+     * @param {ImSendMessageToUserReq} req
+     * @returns {string} messageId
+     */
+    sendC2cMessage(req: ImSendMessageToUserReq): Promise<string>;
+    /**
+     * 发送群聊普通消息
+     * @param {ImSendMessageToGroupReq} req
+     * @returns {string} messageId
+     */
+    sendGroupMessage(req: ImSendMessageToGroupReq): Promise<string>;
+    /**
+     * 查询消息列表
+     * @param {ImListMessageReq} req
+     * @returns {ImListMessageRsp}
+     */
+    listMessage(req: ImListMessageReq): Promise<ImListMessageRsp>;
+    /**
+     * 查询最近消息
+     * @param {string |ImListRecentMessageReq} groupIdOrReq
+     * @returns {ImListRecentMessageRsp}
+     */
+    listRecentMessage(groupIdOrReq: string | ImListRecentMessageReq): Promise<ImListRecentMessageRsp>;
+    /**
+     * 查询历史消息,该接口主要用户直播结束后的历史消息回放,用户无需进入群组可查询,比较耗时,在直播过程中不建议使用,另外该接口后续可能会收费。
+     * @param {ImListHistoryMessageReq} req
+     * @returns {ImListHistoryMessageRsp}
+     */
+    listHistoryMessage(req: ImListHistoryMessageReq): Promise<ImListHistoryMessageRsp>;
+    /**
+     * 删除/撤回群消息
+     */
+    deleteMessage(req: ImDeleteMessageReq): Promise<void>;
+    /**
+     * 创建流式消息
+     */
+    createStreamMessage(req: ImCreateStreamMessageReq): Promise<ImStreamMessageSender>;
+    /**
+     * 拒收流式消息
+     */
+    rejectStreamMessage(req: ImRejectStreamMessageReq): Promise<void>;
+    /**
+     * 自定义流式消息转发
+     */
+    forwardCustomMessage(req: ImForwardCustomMessageReq): Promise<ImForwardCustomMessageRsp>;
+}
+
+/**
+ * Minimal `EventEmitter` interface that is molded against the Node.js
+ * `EventEmitter` interface.
+ */
+class EventEmitter<
+EventTypes extends EventEmitter.ValidEventTypes = string | symbol,
+Context extends any = any
+> {
+    static prefixed: string | boolean;
+
+    /**
+     * Return an array listing the events for which the emitter has registered
+     * listeners.
+     */
+    eventNames(): Array<EventEmitter.EventNames<EventTypes>>;
+
+    /**
+     * Return the listeners registered for a given event.
+     */
+    listeners<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T
+    ): Array<EventEmitter.EventListener<EventTypes, T>>;
+
+    /**
+     * Return the number of listeners listening to a given event.
+     */
+    listenerCount(event: EventEmitter.EventNames<EventTypes>): number;
+
+    /**
+     * Calls each of the listeners registered for a given event.
+     */
+    emit<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    ...args: EventEmitter.EventArgs<EventTypes, T>
+    ): boolean;
+
+    /**
+     * Add a listener for a given event.
+     */
+    on<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+    ): this;
+    addListener<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+    ): this;
+
+    /**
+     * Add a one-time listener for a given event.
+     */
+    once<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context
+    ): this;
+
+    /**
+     * Remove the listeners of a given event.
+     */
+    removeListener<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn?: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context,
+    once?: boolean
+    ): this;
+    off<T extends EventEmitter.EventNames<EventTypes>>(
+    event: T,
+    fn?: EventEmitter.EventListener<EventTypes, T>,
+    context?: Context,
+    once?: boolean
+    ): this;
+
+    /**
+     * Remove all listeners, or those of the specified event.
+     */
+    removeAllListeners(event?: EventEmitter.EventNames<EventTypes>): this;
+}
+
+namespace EventEmitter {
+    interface ListenerFn<Args extends any[] = any[]> {
+        (...args: Args): void;
+    }
+
+    interface EventEmitterStatic {
+        new <
+        EventTypes extends ValidEventTypes = string | symbol,
+        Context = any
+        >(): EventEmitter<EventTypes, Context>;
+    }
+
+    /**
+     * `object` should be in either of the following forms:
+     * ```
+     * interface EventTypes {
+     *   'event-with-parameters': any[]
+     *   'event-with-example-handler': (...args: any[]) => void
+     * }
+     * ```
+     */
+    type ValidEventTypes = string | symbol | object;
+
+    type EventNames<T extends ValidEventTypes> = T extends string | symbol
+    ? T
+    : keyof T;
+
+    type ArgumentMap<T extends object> = {
+        [K in keyof T]: T[K] extends (...args: any[]) => void
+        ? Parameters<T[K]>
+        : T[K] extends any[]
+        ? T[K]
+        : any[];
+    };
+
+    type EventListener<
+    T extends ValidEventTypes,
+    K extends EventNames<T>
+    > = T extends string | symbol
+    ? (...args: any[]) => void
+    : (
+    ...args: ArgumentMap<Exclude<T, string | symbol>>[Extract<K, keyof T>]
+    ) => void;
+
+    type EventArgs<
+    T extends ValidEventTypes,
+    K extends EventNames<T>
+    > = Parameters<EventListener<T, K>>;
+
+    const EventEmitter: EventEmitterStatic;
+}
+
+interface ImAttachmentProgress {
+    progress: number;
+    totalSize: number;
+    currentSize: number;
+}
+
+interface ImAttachmentReq {
+    id: string;
+    fileType: ImAttachmentType;
+    fileName: string;
+    file?: File | Blob;
+    filePath?: string;
+    extra?: string;
+    onProgress?: (res: ImAttachmentProgress) => void;
+}
+
+interface ImAttachmentRes {
+    id: string;
+    fileType: ImAttachmentType;
+    fileSize: number;
+    fileName: string;
+    accessKey: string;
+    fileDuration: number;
+    extra: string;
+}
+
+enum ImAttachmentType {
+    IMAGE = 1,
+    AUDIO = 2,
+    VIDEO = 3,
+    OTHER = 4
+}
+
+interface ImAuth {
+    /**
+     * 随机数,格式:"AK-随机串", 最长64字节, 仅限A-Z,a-z,0-9及"_",可为空
+     */
+    nonce: string;
+    /**
+     * 过期时间:从1970到过期时间的秒数
+     */
+    timestamp: number;
+    /**
+     * 角色,为admin时,表示该用户可以调用管控接口,可为空,如果要给当前用户admin权限,应该传admin
+     */
+    role?: string;
+    /**
+     * token
+     */
+    token: string;
+}
+
+interface ImCancelMuteAllReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImCancelMuteUserReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param userList 被取消禁言的用户列表
+     */
+    userList: string[];
+}
+
+interface ImCloseGroupReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImCreateGroupReq {
+    /**
+     * @param groupId 群组id,【可选】id为空的话,会由sdk内部生成
+     */
+    groupId?: string;
+    /**
+     * @param groupName 群组名称
+     */
+    groupName: string;
+    /**
+     * @param extension 业务扩展字段
+     */
+    groupMeta?: string;
+}
+
+interface ImCreateGroupRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param alreadyExist 是否已经创建过
+     */
+    alreadyExist: boolean;
+}
+
+interface ImCreateStreamMessageReq {
+    /**
+     * 数据类型
+     */
+    dataType: ImStreamMessageDataType;
+    /**
+     * 数据接收类型
+     */
+    receiverType: ImStreamMessageReceiverType;
+    /**
+     * 数据接收者ID
+     */
+    receiverId: string;
+}
+
+interface ImDeleteMessageReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param messageId 消息id
+     */
+    messageId: string;
+}
+
+let ImEngine: typeof ImEngine_2;
+
+class ImEngine_2 extends EventEmitter<ImSdkListener> {
+    private wasmIns;
+    private wasmEngine;
+    private wasmInterface;
+    private transport?;
+    private appEventManager?;
+    private eventListener;
+    private messageManager?;
+    private groupManager?;
+    private attachmentManager?;
+    private pluginProvider?;
+    private uploader;
+    private supportsWebRtc;
+    private supportWASM;
+    private initFlag;
+    constructor();
+    static engine: ImEngine_2;
+    /**
+     * @brief 获取 SDK 引擎实例(单例)
+     * @returns ImEngine
+     */
+    static createEngine(): ImEngine_2;
+    /**
+     * 当前 SDK 是否支持,支持 WASM 或者 ASM
+     * @returns
+     */
+    static isSupport(): boolean;
+    static getSdkVersion(): string;
+    private initTransport;
+    private initAppEvent;
+    private loadWasm;
+    private preloadUploader;
+    private initNativePlugin;
+    /**
+     * @brief 初始化
+     * @param config SDK配置信息
+     */
+    init(config: ImSdkConfig): Promise<0 | ImErrors.ERROR_CLIENT_REPEATED_INIT>;
+    /**
+     * 添加 Engine 事件监听
+     */
+    private addEventListener;
+    private removeEventListener;
+    private destroy;
+    /**
+     * @brief 销毁
+     */
+    unInit(): boolean;
+    /**
+     * @brief 登录
+     * @param req 登录请求数据
+     */
+    login(loginReq: ImLoginReq): Promise<void>;
+    /**
+     * @brief 登出
+     */
+    logout(): Promise<void>;
+    /**
+     * 强制重连
+     */
+    reconnect(): void;
+    /**
+     * @brief 获取当前登录用户 ID
+     */
+    getCurrentUserId(): string;
+    /**
+     * @brief 是否登录
+     */
+    isLogin(): boolean;
+    /**
+     * @brief 是否已退出登录
+     */
+    isLogout(): boolean;
+    /**
+     * @brief 获取消息管理器 {AliVCIMMessageInterface}
+     * @return 返回消息管理器实例
+     */
+    getMessageManager(): AliVCIMMessageManager | undefined;
+    /**
+     * @brief 获取群组管理器 {AliVCIMGroupInterface}
+     * @return 返回群组管理器实例
+     */
+    getGroupManager(): AliVCIMGroupManager | undefined;
+    /**
+     * @brief 获取附件管理器 {AliVCIMAttachmentInterface}
+     * @return 返回附件管理器实例
+     */
+    getAttachmentManager(): AliVCIMAttachmentManager | undefined;
+}
+
+enum ImErrors {
+    /**
+     * 已经登录
+     */
+    ERROR_HAS_LOGIN = 304,
+    /**
+     * 参数错误;参数无法解析
+     */
+    ERROR_INVALID_PARAM = 400,
+    /**
+     * 错误码(subcode)	说明
+     * 403	操作无权限; 或登录时鉴权失败
+     */
+    ERROR_NO_PERMISSION = 403,
+    /**
+     * no session,可能因为客户网络变化等原因导致的连接变化,服务器在新连接上收到消息无法正常处理,需要reconnect 信令。
+     */
+    ERROR_NO_SESSION = 404,
+    /**
+     * 审核不通过
+     */
+    ERROR_AUDIT_FAIL = 406,
+    /**
+     * 繁忙,发送太快,稍候重试
+     * 服务端同学确认不需要区分这两个错误
+     */
+    ERROR_INTERNAL_BUSY = 412,
+    ERROR_INTERNAL_BUSY2 = 413,
+    /**
+     * 发送 c2c 消息对方用户不在线
+     */
+    ERROR_USER_OFFLINE = 424,
+    /**
+     * 未加入群组
+     */
+    ERROR_GROUP_NOT_JOINED = 425,
+    /**
+     * 操作过快,短时间内,发起过多请求。如同一个用户,1秒内发起2次登录。
+     */
+    ERROR_INTERNAL_BUSY3 = 429,
+    /**
+     * 群组不存在
+     */
+    ERROR_GROUP_NOT_EXIST = 440,
+    /**
+     * 群组已删除
+     */
+    ERROR_GROUP_DELETED = 441,
+    /**
+     * 无法在该群组中发送消息,被禁言
+     */
+    ERROR_SEND_GROUP_MSG_FAIL = 442,
+    /**
+     * 进了太多的群组, 列表人数超大等
+     */
+    ERROR_REACH_MAX = 443,
+    /**
+     * 无法加入该群,被禁止加入(暂无需求未实现)预留
+     */
+    ERROR_JOIN_GROUP_FAIL = 450,
+    /**
+     * ots 查询错误
+     */
+    ERROR_OTS_FAIL = 480,
+    /**
+     * 系统临时错误,稍候重试
+     */
+    ERROR_INTERNALE_RROR = 500,
+    /**
+     * 底层重复初始化
+     */
+    ERROR_CLIENT_REPEATED_INIT = -1,
+    /**
+     * 初始化配置信息有误
+     */
+    ERROR_CLIENT_INIT_INVALID_PARAM = -2,
+    /**
+     * 未初始化
+     */
+    ERROR_CLIENT_NOT_INIT = 1,
+    /**
+     * 参数异常
+     */
+    ERROR_CLIENT_INVALID_PARAM = 2,
+    /**
+     * 状态有误
+     */
+    ERROR_CLIENT_INVALID_STATE = 3,
+    /**
+     * 建连失败
+     */
+    ERROR_CLIENT_CONNECT_ERROR = 4,
+    /**
+     * 建连超时
+     */
+    ERROR_CLIENT_CONNECT_TIMEOUT = 5,
+    /**
+     * 发送失败
+     */
+    ERROR_CLIENT_SEND_FAILED = 6,
+    /**
+     * 发送取消
+     */
+    ERROR_CLIENT_SEND_CANCEL = 7,
+    /**
+     * 发送超时
+     */
+    ERROR_CLIENT_SEND_TIMEOUT = 8,
+    /**
+     * 订阅失败
+     */
+    ERROR_CLIENT_SUB_ERROR = 9,
+    /**
+     * 订阅通道断连
+     */
+    ERROR_CLIENT_SUB_DISCONNECT = 10,
+    /**
+     * 订阅超时
+     */
+    ERROR_CLIENT_SUB_TIMEOUT = 11,
+    /**
+     * 压缩失败
+     */
+    ERROR_CLIENT_COMPRESS_ERROR = 12,
+    /**
+     * 解压失败
+     */
+    ERROR_CLIENT_DECOMPRESS_ERROR = 13,
+    /**
+     * 加密失败
+     */
+    ERROR_CLIENT_ENCRYPT_ERROR = 14,
+    /**
+     * 解密失败
+     */
+    ERROR_CLIENT_DECRYPT_ERROR = 15,
+    /**
+     * 消息体封装失败
+     */
+    ERROR_CLIENT_CONVERTER_ERROR = 16,
+    /**
+     * 消息体解析失败
+     */
+    ERROR_CLIENT_PARSE_ERROR = 17,
+    /**
+     * 数据为空
+     */
+    ERROR_CLIENT_DATA_EMPTY = 18,
+    /**
+     * 数据错误
+     */
+    ERROR_CLIENT_DATA_ERROR = 19,
+    /**
+     * 地址出错(可能是AppSign有误,如头部带了空格、内容被截断等)
+     */
+    ERROR_CLIENT_URL_ERROR = 20,
+    /**
+     * 建连取消
+     */
+    CONNECT_CANCEL = 21,
+    /**
+     * 重试超过次数限制
+     */
+    RETRY_OVER_TIME = 22,
+    /**
+     * 状态错误
+     */
+    ERROR_INVALID_STATE = 601,
+    /**
+     * 未登录
+     */
+    ERROR_NOT_LOGIN = 602,
+    /**
+     * 收到上次session的消息
+     */
+    ERROR_RECEIVE_LAST_SESSION = 603,
+    /**
+     * Parse Data Error
+     */
+    ERROR_PARSE_DATA_ERROR = 604
+}
+
+interface ImForwardCustomMessageReq {
+    /**
+     * 数据接收者ID
+     */
+    receiverId: string;
+    /**
+     * 数据,若需要传对象,需要序列化
+     */
+    data: string;
+}
+
+interface ImForwardCustomMessageRsp {
+    /**
+     * 流式消息ID
+     */
+    messageId: string;
+    /**
+     * 数据,若返回的是对象,需要反序列化
+     */
+    data: string;
+}
+
+interface ImGroupInfo {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param groupName 群组名称
+     */
+    groupName: string;
+    /**
+     * @param groupMeta 群组透传信息
+     */
+    groupMeta: string;
+    /**
+     * @param createTime 创建时间
+     */
+    createTime: number;
+    /**
+     * @param creator 创建者id
+     */
+    creator: string;
+    /**
+     * @param admins 管理员列表
+     */
+    admins: string[];
+    /**
+     * @param statistics 群组统计
+     */
+    statistics: ImGroupStatistics;
+    /**
+     * @param muteStatus 群禁言信息
+     */
+    muteStatus: ImGroupMuteStatus;
+}
+
+interface ImGroupInfoStatus {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param groupMeta 群组扩展信息
+     */
+    groupMeta: string;
+    /**
+     * @param adminList 管理员列表
+     */
+    adminList: string[];
+}
+
+interface ImGroupListener {
+    /**
+     * @deprecated 1.4.1 后请使用 memberdatachange 事件
+     *
+     * 群组成员变化
+     * @param groupId  群组ID
+     * @param memberCount 当前群组人数
+     * @param joinUsers 加入的用户
+     * @param leaveUsers 离开的用户
+     */
+    memberchange: (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => void;
+    /**
+     * 1.4.1 版本新增新的群组成员变化,返回的是一个对象
+     * @param data 群组成员变化数据对象
+     * @param data.groupId  群组ID
+     * @param data.onlineCount 当前群组在线人数
+     * @param data.pv  加入群组累积pv数
+     * @param data.isBigGroup 是否是大群组
+     * @param data.joinUsers 加入的用户
+     * @param data.leaveUsers 离开的用户
+     */
+    memberdatachange: (data: ImMemberChangeData) => void;
+    /**
+     * 退出群组
+     * @param groupId  群组ID
+     * @param reason 退出原因 1: 群被解散, 2:被踢出来了
+     */
+    exit: (groupId: string, reason: number) => void;
+    /**
+     * 群组静音状态变化
+     * @param groupId  群组ID
+     * @param status 静音状态
+     */
+    mutechange: (groupId: string, status: ImGroupMuteStatus) => void;
+    /**
+     * 群组信息变化
+     * @param groupId  群组ID
+     * @param info 群组信息
+     */
+    infochange: (groupId: string, info: ImGroupInfoStatus) => void;
+}
+
+interface ImGroupMuteStatus {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param muteAll 是否全员禁言
+     */
+    muteAll: boolean;
+    /**
+     * @param muteUserList 禁言用户ID列表
+     */
+    muteUserList: string[];
+    /**
+     * @param whiteUserList 白名单用户ID列表
+     */
+    whiteUserList: string[];
+}
+
+interface ImGroupStatistics {
+    /**
+     * @param pv PV
+     */
+    pv: number;
+    /**
+     * @param onlineCount 在线人数
+     */
+    onlineCount: number;
+    /**
+     * @param msgAmount 消息数量
+     */
+    msgAmount: {
+        [key: string]: number;
+    };
+}
+
+interface ImJoinGroupReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImLeaveGroupReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImListGroupUserReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param sortType 排序方式,ASC-先加入优先,DESC-后加入优先
+     */
+    sortType?: ImSortType;
+    /**
+     * @param nextPageToken 默认表示第一页,遍历时服务端会返回,客户端获取下一页时,应带上
+     */
+    nextPageToken?: number;
+    /**
+     * @deprecated 请使用 nextPageToken
+     */
+    nextpagetoken?: number;
+    /**
+     * @param pageSize 最大不超过50
+     */
+    pageSize?: number;
+}
+
+interface ImListGroupUserRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param nextPageToken 下一页的token
+     */
+    nextPageToken: number;
+    /**
+     * @deprecated 请使用 nextPageToken
+     */
+    nextpagetoken?: number;
+    /**
+     * @param hasMore 是否还有下一页
+     */
+    hasMore: boolean;
+    /**
+     * @param userList 返回的群组的在线成员列表
+     */
+    userList: ImUser[];
+}
+
+interface ImListHistoryMessageReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param type 消息类型
+     */
+    type: number;
+    /**
+     * @param nextPageToken 不传时表示第一页,遍历时服务端会返回,客户端获取下一页时,应带上
+     */
+    nextPageToken?: number;
+    /**
+     * @param sortType 排序类型,默认为时间递增
+     */
+    sortType?: ImSortType;
+    /**
+     * @param pageSize 取值范围 10~30
+     */
+    pageSize?: number;
+    /**
+     * @param begintime 按时间范围遍历,开始时间,不传时表示最早时间,单位:秒
+     */
+    beginTime?: number;
+    /**
+     * @param endtime 按时间范围遍历,结束时间,不传时表示最晚时间,单位:秒
+     */
+    endTime?: number;
+}
+
+interface ImListHistoryMessageRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param nextPageToken 不传时表示第一页,遍历时服务端会返回,客户端获取下一页时,应带上
+     */
+    nextPageToken?: number;
+    /**
+     *@param hasMore 是否有更多数据
+     */
+    hasMore: boolean;
+    /**
+     *@param messageList 返回消息列表
+     **/
+    messageList: ImMessage[];
+}
+
+interface ImListMessageReq {
+    /**
+     * @param groupId 话题id,聊天插件实例id
+     */
+    groupId: string;
+    /**
+     * @param type 消息类型
+     */
+    type: number;
+    /**
+     * @param nextPageToken 不传时表示第一页,遍历时服务端会返回,客户端获取下一页时应带上
+     */
+    nextPageToken?: number;
+    /**
+     * @deprecated 请使用nextPageToken
+     */
+    nextpagetoken?: number;
+    /**
+     * @param sortType 排序类型,默认为时间递增
+     */
+    sortType?: ImSortType;
+    /**
+     * @param pageSize 分页拉取的大小,默认10条,最大30条
+     */
+    pageSize?: number;
+    /**
+     * @param begintime 按时间范围遍历,开始时间,不传时表示最早时间,单位:秒
+     */
+    beginTime?: number;
+    /**
+     * @param endtime 按时间范围遍历,结束时间,不传时表示最晚时间,单位:秒
+     */
+    endTime?: number;
+}
+
+interface ImListMessageRsp {
+    /**
+     ** @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     *@param nextpagetoken 客户端获取下一页时,应带上
+     */
+    nextPageToken: number;
+    /**
+     * @deprecated 请使用 nextPageToken
+     */
+    nextpagetoken?: number;
+    /**
+     *@param hasmore 是否有更多数据
+     */
+    hasMore: boolean;
+    /**
+     *@param messageList 返回消息列表
+     **/
+    messageList: ImMessage[];
+}
+
+interface ImListMuteUsersReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImListMuteUsersRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param muteAll 是否全员禁言
+     */
+    muteAll: boolean;
+    /**
+     * @param muteUserList 禁言用户ID列表
+     */
+    muteUserList: string[];
+    /**
+     * @param whiteUserList 白名单用户ID列表
+     */
+    whiteUserList: string[];
+}
+
+interface ImListRecentGroupUserReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImListRecentGroupUserRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param total 群组成员总数
+     */
+    total: number;
+    /**
+     * @param userList 返回的群组的在线成员列表
+     */
+    userList: ImUser[];
+}
+
+interface ImListRecentMessageReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param seqnum 消息序列号
+     */
+    seqnum?: number;
+    /**
+     * @param pageSize 分页拉取的大小,默认50条
+     */
+    pageSize?: number;
+}
+
+interface ImListRecentMessageRsp {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param messageList 返回消息列表
+     */
+    messageList: ImMessage[];
+}
+
+interface ImLoginReq {
+    user: ImUser;
+    /**
+     * 用户鉴权信息
+     */
+    userAuth: ImAuth;
+}
+
+enum ImLogLevel {
+    NONE = 0,
+    DBUG = 1,
+    INFO = 2,
+    WARN = 3,
+    ERROR = 4
+}
+
+interface ImMemberChangeData {
+    groupId: string;
+    onlineCount: number;
+    pv: number;
+    isBigGroup: boolean;
+    joinUsers: ImUser[];
+    leaveUsers: ImUser[];
+}
+
+interface ImMessage {
+    /**
+     * @param groupId 话题id,聊天插件实例id
+     */
+    groupId?: string;
+    /**
+     * @param messageId 消息id
+     */
+    messageId: string;
+    /**
+     *@param type 消息类型。系统消息小于10000
+     */
+    type: number;
+    /**
+     *@param sender 发送者
+     */
+    sender?: ImUser;
+    /**
+     **@param data 消息内容
+     */
+    data: string;
+    /**
+     *@param seqnum 消息顺序号
+     */
+    seqnum: number;
+    /**
+     *@param timestamp 消息发送时间
+     */
+    timestamp: number;
+    /**
+     *@param level 消息分级
+     **/
+    level: ImMessageLevel;
+    /**
+     * @param repeatCount 消息统计数量增长值,默认1,主要用于聚合同类型消息。
+     */
+    repeatCount: number;
+    /**
+     * @param totalMsgs 同类型的消息数量
+     */
+    totalMsgs: number;
+}
+
+enum ImMessageLevel {
+    NORMAL = 0,
+    HIGH = 1
+}
+
+interface ImMessageListener {
+    /**
+     * 接收到c2c消息
+     * @param msg 消息
+     */
+    recvc2cmessage: (msg: ImMessage) => void;
+    /**
+     * 接收到群消息
+     * @param msg 消息
+     * @param groupId 群id
+     */
+    recvgroupmessage: (msg: ImMessage, groupId: string) => void;
+    /**
+     * 删除群消息
+     * @param msgId 消息id
+     * @param groupId 群id
+     */
+    deletegroupmessage: (msgId: string, groupId: string) => void;
+    /**
+     * 流消息结束通知
+     * @param {string} messageId 消息ID
+     * @param {number} endCode 结束原因:0正常处理结束,1主动取消,2与智能体服务连接异常断开,3连接超时断开,4收到新的开始切片,5包请求异常
+     * @param {number} [subCode] 详情码
+     * @param {string} [subMsg]  详情信息
+     */
+    streammessageend: (messageId: string, endCode: number, subCode?: number, subMsg?: string) => void;
+    /**
+     * 接收到流消息
+     * @param message 流消息
+     */
+    recvstreammessage: (message: ImStreamMessage) => void;
+}
+
+interface ImModifyGroupReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param forceUpdateGroupMeta 为true表示强制刷新groupMeta信息,若groupMeta为空则表示清空;
+     *                             为false,则只有groupMeta不空才更新groupMeta信息
+     */
+    forceUpdateGroupMeta?: boolean;
+    /**
+     * @param groupMeta 群信息扩展字段
+     */
+    groupMeta?: string;
+    /**
+     * @param forceUpdateAdmins 为true表示强制刷新admins信息,若admins为空则表示清空;
+     *                          为false,则只有admins不空才更新admins信息
+     */
+    forceUpdateAdmins?: boolean;
+    /**
+     * @param admins 群管理员ID列表,最多设置3个管理员
+     */
+    admins?: string[];
+}
+
+interface ImMuteAllReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImMuteUserReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+    /**
+     * @param userList 需要禁言的用户列表
+     */
+    userList: string[];
+}
+
+interface ImQueryGroupReq {
+    /**
+     * @param groupId 群组id
+     */
+    groupId: string;
+}
+
+interface ImRejectStreamMessageReq {
+    /**
+     * 流式消息ID
+     */
+    messageId: string;
+    /**
+     * 数据接收类型
+     */
+    receiverType: ImStreamMessageReceiverType;
+    /**
+     * 错误码
+     */
+    code: number;
+    /**
+     * 错误信息
+     */
+    msg: string;
+}
+
+interface ImSdkConfig {
+    /**
+     * 设备唯一标识
+     */
+    deviceId?: string;
+    /**
+     * 应用ID
+     */
+    appId: string;
+    /**
+     * 应用签名
+     */
+    appSign: string;
+    /**
+     * 日志级别
+     */
+    logLevel?: ImLogLevel;
+    /**
+     * 来源
+     */
+    source?: string;
+    /**
+     * 心跳超时时间,单位是秒,默认 99s,允许 [15-120]s
+     */
+    heartbeatTimeout?: number;
+    /**
+     * @param extra 用户自定义参数
+     */
+    extra?: {
+        [key: string]: string;
+    };
+    /**
+     * @param uploader 附件上传器参数
+     */
+    uploader?: {
+        /**
+         * 是否提前加载,默认 false
+         */
+        preload?: boolean;
+        /**
+         * 指定sdk文件地址
+         */
+        sdkUrl?: string;
+    };
+}
+
+interface ImSdkListener {
+    /**
+     * 连接中
+     */
+    connecting: () => void;
+    /**
+     * 连接成功
+     */
+    connectsuccess: () => void;
+    /**
+     * 连接失败
+     */
+    connectfailed: (error: Error) => void;
+    /**
+     * 连接断开
+     * @param code 断开原因 1:主动退出, 2:被踢出 3:超时等其他原因 4:在其他端上登录
+     */
+    disconnect: (code: number) => void;
+    /**
+     * 连接状态变化
+     * state 状态 0:未连接 1:连接中 2:已连接 3:已断联
+     */
+    linkstate: (data: {
+        previousState: number;
+        currentState: number;
+    }) => void;
+    /**
+     * token过期
+     * @param callback 更新 Token 的回调
+     */
+    tokenexpired: (callback: TokenCallback) => void;
+    /**
+     * 重连成功
+     */
+    reconnectsuccess: (groupInfos: ImGroupInfo[]) => void;
+}
+
+interface ImSendMessageToGroupReq {
+    /**
+     * @param groupId 话题id,聊天插件实例id
+     */
+    groupId: string;
+    /**
+     * @param type 消息类型,小于等于10000位系统消息,大于10000位自定义消息
+     */
+    type: number;
+    /**
+     * @param data 消息体
+     */
+    data: string;
+    /**
+     * @param skipMuteCheck 跳过禁言检测,true:忽略被禁言用户,还可发消息;false:当被禁言时,消息无法发送,默认为false,即为不跳过禁言检测。
+     */
+    skipMuteCheck?: boolean;
+    /**
+     * @param skipAudit 跳过安全审核,true:发送的消息不经过阿里云安全审核服务审核;false:发送的消息经过阿里云安全审核服务审核,审核失败则不发送;
+     */
+    skipAudit?: boolean;
+    /**
+     * @param level 消息分级
+     */
+    level?: ImMessageLevel;
+    /**
+     * @param noStorage 为true时,表示该消息不需要存储,也无法拉取查询
+     */
+    noStorage?: boolean;
+    /**
+     * @param repeatCount 消息统计数量增长值,默认1,主要用于聚合同类型消息,只发送一次请求,例如点赞场景
+     */
+    repeatCount?: number;
+}
+
+interface ImSendMessageToUserReq {
+    /**
+     * 消息类型。系统消息小于10000
+     */
+    type: number;
+    /**
+     * 消息体
+     */
+    data: string;
+    /**
+     * 接收者用户
+     */
+    receiverId: string;
+    /**
+     * 跳过安全审核,true:发送的消息不经过阿里云安全审核服务审核;false:发送的消息经过阿里云安全审核服务审核,审核失败则不发送;
+     */
+    skipAudit?: boolean;
+    /**
+     * 消息分级
+     */
+    level?: ImMessageLevel;
+}
+
+enum ImSortType {
+    ASC = 0,
+    DESC = 1
+}
+
+interface ImStreamData {
+    seqNum: number;
+    byteData: ArrayBuffer;
+}
+
+interface ImStreamMessage {
+    /**
+     * 流式消息ID
+     */
+    messageId: string;
+    /**
+     * 发送用户
+     */
+    sender: ImUser;
+    /**
+     * 流式消息帧数据
+     */
+    data: ImStreamData;
+}
+
+enum ImStreamMessageDataType {
+    TEXT = 1,// 文本
+    BINARY_FILE = 2
+}
+
+enum ImStreamMessageReceiverType {
+    SERVER = 0
+}
+
+class ImStreamMessageSender extends EventEmitter<ImStreamMessageSenderListener> {
+    private wasmIns;
+    private sender;
+    constructor(wasmIns: any);
+    setSender(sender: any): void;
+    getMessageId(): string;
+    /**
+     * 发送字节数据
+     * @param {Uint8Array} byteData 字节数据
+     * @param {boolean} isLast 是否结束流
+     * @param {ImAttachmentRes[]} [attachments] 附件列表
+     */
+    sendByteData(byteData: Uint8Array, isLast: boolean, attachments?: ImAttachmentRes[]): void;
+    /**
+     * 取消发送
+     * @param {number} [code] 取消码
+     * @param {string} [msg] 取消原因
+     */
+    cancel(code?: number, msg?: string): void;
+    destroy(): void;
+}
+
+interface ImStreamMessageSenderListener {
+    /**
+     * 流消息结束通知
+     * @param {string} messageId 消息ID
+     * @param {number} endCode 结束原因:0正常处理结束,1主动取消,2与智能体服务连接异常断开,3连接超时断开,4收到新的开始切片,5包请求异常
+     * @param {number} [subCode] 详情码
+     * @param {string} [subMsg]  详情信息
+     */
+    streammessageend: (messageId: string, endCode: number, subCode?: number, subMsg?: string) => void;
+}
+
+enum ImStreamMessageStatus {
+    CONTINUE = 0,// 中间帧
+    START = 1,// 开始帧
+    END = 2,// 结束帧
+    ALL = 3,// 一次性传输
+    CANCEL = 4
+}
+
+enum ImStreamMessageType {
+    NORMAL = 0
+}
+
+interface ImUser {
+    /**
+     * @param user_id 用户id
+     */
+    userId: string;
+    /**
+     * @param user_extension 用户扩展信息
+     */
+    userExtension?: string;
+}
+
+type TokenCallback = (error: {
+    code?: number;
+    msg: string;
+} | null, auth?: ImAuth) => void;
+}

+ 6 - 0
deno.json

@@ -1,5 +1,9 @@
 {
   "imports": {
+    "@alicloud/live20161101": "npm:@alicloud/live20161101@^1.1.1",
+    "@alicloud/openapi-client": "npm:@alicloud/openapi-client@^0.4.14",
+    "@alicloud/live-interaction20201214":"npm:@alicloud/live-interaction20201214@2.1.6",
+    "@alicloud/pop-core":"npm:@alicloud/pop-core@1.8.0",
     "hono": "https://esm.d8d.fun/hono@4.7.4",
     "hono/jsx": "https://esm.d8d.fun/hono@4.7.4/jsx",
     "hono/jsx/jsx-runtime": "https://esm.d8d.fun/hono@4.7.4/jsx/jsx-runtime",
@@ -30,6 +34,8 @@
     "react-hook-form": "https://esm.d8d.fun/react-hook-form@7.55.0?dev&deps=react@19.0.0,react-dom@19.0.0",
     "@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@19.0.0,react-dom@19.0.0",
     "@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@19.0.0,react-dom@19.0.0",
+    "react-toastify": "https://esm.d8d.fun/react-toastify@11.0.5?dev&deps=react@19.0.0,react-dom@19.0.0",
+    "aliyun-rtc-sdk":"https://esm.d8d.fun/aliyun-rtc-sdk@6.14.6?standalone",
     "@testing-library/react": "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0",
     "@testing-library/user-event":"npm:@testing-library/user-event@14.6.1",
     "jsdom":"npm:jsdom@26.0.0"

Разлика између датотеке није приказан због своје велике величине
+ 387 - 1961
deno.lock


+ 2 - 0
server/app.tsx

@@ -400,6 +400,8 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
                 `}} />
               </>
             )}
+            <script src="https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js"></script>
+            <script src="https://g.alicdn.com/apsara-media-box/imp-web-rtc/6.14.1/aliyun-rtc-sdk.js"></script>
           </head>
           <body className="bg-gray-50">
             <div id="root"></div>

+ 263 - 0
server/room_management.ts

@@ -0,0 +1,263 @@
+import LiveClient, * as $LiveClient from "@alicloud/live20161101";
+import LiveInteractionClient, * as $LiveInteraction from "@alicloud/live-interaction20201214";
+import * as $OpenApi from "@alicloud/openapi-client";
+
+interface RoomOptions {
+  title?: string;
+  templateId?: string;
+  maxUsers?: number;
+}
+
+interface UserInfo {
+  id: string;
+  name?: string;
+  avatar?: string;
+  isAdmin?: boolean;
+}
+
+interface Message {
+  id: string;
+  sender: UserInfo;
+  content: string;
+  timestamp: Date;
+}
+
+export class RoomManagementSystem {
+  private liveClient: LiveClient.default;
+  private liveInteractionClient: LiveInteractionClient.default;
+  private appId: string;
+
+  constructor() {
+    const config = new $OpenApi.Config({
+      accessKeyId: Deno.env.get("ALIYUN_LIVE_ACCESS_KEY_ID") || "",
+      accessKeySecret: Deno.env.get("ALIYUN_LIVE_ACCESS_KEY_SECRET") || "",
+      endpoint: "live.aliyuncs.com"
+    });
+    
+    this.liveClient = new LiveClient.default(config);
+    this.liveInteractionClient = new LiveInteractionClient.default(config);
+    this.appId = Deno.env.get("ALIYUN_CHAT_APP_ID") || "";
+  }
+
+  /**
+   * 创建房间
+   * @param roomId 房间ID
+   * @param options 房间选项
+   * @returns 房间信息
+   */
+  async createRoom(roomId: string, options: RoomOptions = {}): Promise<{
+    roomId: string;
+    pushUrl: string;
+    playUrl: string;
+    chatToken: string;
+  }> {
+    try {
+      // 创建互动直播房间
+      const createRequest = new $LiveInteraction.CreateRoomRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        Title: options.title || `Room ${roomId}`,
+        TemplateId: options.templateId || "standard"
+      });
+      const createResponse = await this.liveInteractionClient.createRoom(createRequest);
+
+      // 获取推流/播放地址
+      const urlRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: this.appId,
+        RoomId: roomId
+      });
+      const urlResponse = await this.liveClient.describeLiveStreamsOnlineList(urlRequest);
+
+      return {
+        roomId,
+        pushUrl: urlResponse.PushUrl,
+        playUrl: urlResponse.PlayUrl,
+        chatToken: createResponse.Token
+      };
+    } catch (error) {
+      throw new Error(`Failed to create room: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 销毁房间
+   * @param roomId 房间ID
+   */
+  async destroyRoom(roomId: string): Promise<void> {
+    try {
+      const request = new $LiveInteraction.DestroyRoomRequest({
+        AppId: this.appId,
+        RoomId: roomId
+      });
+      await this.liveInteractionClient.destroyRoom(request); 
+    } catch (error) {
+      throw new Error(`Failed to destroy room: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 用户加入房间
+   * @param roomId 房间ID
+   * @param user 用户信息
+   * @returns 用户Token和播放地址
+   */
+  async joinRoom(roomId: string, user: UserInfo) {
+    try {
+      const addMemberRequest = new $LiveInteraction.AddGroupMembersRequest({
+        appId:this.appId,
+        requestParams: {
+            groupId: roomId,
+            members: [user.id]
+        }
+      })
+
+      await this.liveInteractionClient.addGroupMembers(addMemberRequest) ;
+
+    } catch (error) {
+      throw new Error(`Failed to join room: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 用户离开房间
+   * @param roomId 房间ID
+   * @param userId 用户ID
+   */
+  async leaveRoom(roomId: string, userId: string): Promise<void> {
+    try {
+      const request = new $LiveInteraction.RemoveGroupMembersRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        UserId: userId
+      });
+      await this.liveInteractionClient.leaveRoom(request);
+    } catch (error) {
+      throw new Error(`Failed to leave room: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 发送消息
+   * @param roomId 房间ID
+   * @param sender 发送者信息
+   * @param content 消息内容
+   * @returns 消息ID
+   */
+  async sendMessage(roomId: string, sender: UserInfo, content: string): Promise<string> {
+    try {
+      const request = new $LiveInteraction.SendMessageRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        SenderId: sender.id,
+        Content: content
+      });
+      const response = await this.liveInteractionClient.sendMessage(request);
+      return response.MessageId;
+    } catch (error) {
+      throw new Error(`Failed to send message: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 获取历史消息
+   * @param roomId 房间ID
+   * @param limit 消息数量限制
+   * @returns 消息列表
+   */
+  async getHistoryMessages(roomId: string, limit: number = 50): Promise<Message[]> {
+    try {
+      const request = new $LiveInteraction.GetHistoryMessagesRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        Limit: limit
+      });
+      const response = await this.liveInteractionClient.getHistoryMessages(request);
+      
+      return response.Messages.map((msg: any) => ({
+        id: msg.MessageId,
+        sender: {
+          id: msg.SenderId,
+          name: msg.SenderName,
+          avatar: msg.SenderAvatar
+        },
+        content: msg.Content,
+        timestamp: new Date(msg.Timestamp)
+      }));
+    } catch (error) {
+      throw new Error(`Failed to get history messages: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 禁言用户
+   * @param roomId 房间ID
+   * @param userId 用户ID
+   * @param duration 禁言时长(秒)
+   */
+  async muteUser(roomId: string, userId: string, duration: number = 3600): Promise<void> {
+    try {
+      const request = new $LiveInteraction.MuteUserRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        UserId: userId,
+        Duration: duration
+      });
+      await this.liveInteractionClient.muteUser(request);
+    } catch (error) {
+      throw new Error(`Failed to mute user: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 踢出用户
+   * @param roomId 房间ID
+   * @param userId 用户ID
+   */
+  async kickUser(roomId: string, userId: string): Promise<void> {
+    try {
+      const request = new $LiveInteraction.KickUserRequest({
+        AppId: this.appId,
+        RoomId: roomId,
+        UserId: userId
+      });
+      await this.liveInteractionClient.kickUser(request);
+    } catch (error) {
+      throw new Error(`Failed to kick user: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+
+  /**
+   * 获取房间信息
+   * @param roomId 房间ID
+   * @returns 房间信息
+   */
+  async getRoomInfo(roomId: string): Promise<{
+    roomId: string;
+    playUrl: string;
+    onlineCount: number;
+  }> {
+    try {
+      // 获取直播间信息
+      const infoRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: this.appId,
+        RoomId: roomId
+      });
+      const infoResponse = await this.liveClient.describeLiveStreamsOnlineList(infoRequest);
+
+      // 获取在线人数
+      const statsRequest = new $LiveClient.DescribeLiveDomainOnlineUserNumRequest({
+        AppId: this.appId,
+        RoomId: roomId
+      });
+      const statsResponse = await this.liveClient.describeLiveDomainOnlineUserNum(statsRequest);
+
+      return {
+        roomId,
+        playUrl: infoResponse.PlayUrl,
+        onlineCount: statsResponse.OnlineCount
+      };
+    } catch (error) {
+      throw new Error(`Failed to get room info: ${error instanceof Error ? error.message : "Unknown error"}`);
+    }
+  }
+}

+ 171 - 0
server/routes_live.ts

@@ -0,0 +1,171 @@
+import { Hono } from "hono";
+import LiveClient, * as $LiveClient from "@alicloud/live20161101";
+import LiveInteractionClient, * as $LiveInteraction from "@alicloud/live-interaction20201214";
+import * as $OpenApi from "@alicloud/openapi-client";
+import type { Variables, WithAuth } from "./app.tsx";
+
+// 阿里云SDK配置
+const liveConfig = new $OpenApi.Config({
+  accessKeyId: Deno.env.get("ALIYUN_LIVE_ACCESS_KEY_ID") || "",
+  accessKeySecret: Deno.env.get("ALIYUN_LIVE_ACCESS_KEY_SECRET") || "",
+  endpoint: "live.aliyuncs.com"
+});
+const liveInteractionClient = new LiveInteractionClient.default(liveConfig);
+const liveClient = new LiveClient.default(liveConfig);
+
+// 创建直播路由
+export function createLiveRoutes(withAuth: WithAuth) {
+  const liveRoutes = new Hono<{ Variables: Variables }>();
+
+  // 创建房间
+  liveRoutes.post("/create-room", withAuth, async (c) => {
+    const { roomId } = await c.req.json();
+    
+    try {
+      // 创建直播间
+      const createRequest = new $LiveInteraction.CreateRoomRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId,
+        Title: `Live Room ${roomId}`,
+        TemplateId: "standard"
+      });
+      const createResponse = await liveInteractionClient.createRoom(createRequest);
+
+      // 获取推流/播放地址
+      const urlRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const urlResponse = await liveClient.describeLiveStreamsOnlineList(urlRequest);
+
+      return c.json({ 
+        success: true,
+        roomId,
+        pushUrl: urlResponse.PushUrl,
+        playUrl: urlResponse.PlayUrl,
+        chatToken: createResponse.Token
+      }, 201);
+    } catch (error) {
+      return c.json({ 
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error"
+      }, 500);
+    }
+  });
+
+  // 加入房间
+  liveRoutes.post("/join-room", withAuth, async (c) => {
+    const { roomId } = await c.req.json();
+    
+    try {
+      // 获取直播间信息
+      const infoRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const infoResponse = await liveClient.describeLiveStreamsOnlineList(infoRequest);
+
+      // 获取用户Token - 使用正确的API方法
+      const tokenRequest = new $LiveInteraction.CreateTokenRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId,
+        UserId: c.get("user")?.id || "anonymous"
+      });
+      const tokenResponse = await liveInteractionClient.createToken(tokenRequest);
+
+      liveInteractionClient.getLoginToken()
+
+      return c.json({ 
+        success: true,
+        playUrl: infoResponse.PlayUrl,
+        chatToken: tokenResponse.Token
+      });
+    } catch (error) {
+      return c.json({ 
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error"
+      }, 500);
+    }
+  });
+
+  // 获取房间信息
+  liveRoutes.get("/room-info/:id", withAuth, async (c) => {
+    const roomId = c.req.param("id");
+    
+    try {
+      // 获取直播间信息
+      const infoRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const infoResponse = await liveClient.describeLiveStreamsOnlineList(infoRequest);
+
+      // 获取在线人数
+      const statsRequest = new $LiveClient.DescribeLiveDomainOnlineUserNumRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const statsResponse = await liveClient.describeLiveDomainOnlineUserNum(statsRequest);
+
+      return c.json({ 
+        success: true,
+        roomId,
+        playUrl: infoResponse.PlayUrl,
+        onlineCount: statsResponse.OnlineCount
+      });
+    } catch (error) {
+      return c.json({ 
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error"
+      }, 500);
+    }
+  });
+
+  // 获取推流地址
+  liveRoutes.get("/push-url/:roomId", withAuth, async (c) => {
+    const roomId = c.req.param("roomId");
+    
+    try {
+      const urlRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const urlResponse = await liveClient.describeLiveStreamsOnlineList(urlRequest);
+
+      return c.json({
+        success: true,
+        pushUrl: urlResponse.PushUrl
+      });
+    } catch (error) {
+      return c.json({
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error"
+      }, 500);
+    }
+  });
+
+  // 获取拉流地址
+  liveRoutes.get("/pull-url/:roomId", withAuth, async (c) => {
+    const roomId = c.req.param("roomId");
+    
+    try {
+      const urlRequest = new $LiveClient.DescribeLiveStreamsOnlineListRequest({
+        AppId: Deno.env.get("ALIYUN_CHAT_APP_ID") || "",
+        RoomId: roomId
+      });
+      const urlResponse = await liveClient.describeLiveStreamsOnlineList(urlRequest);
+
+      return c.json({
+        success: true,
+        pullUrl: urlResponse.PlayUrl
+      });
+    } catch (error) {
+      return c.json({
+        success: false,
+        error: error instanceof Error ? error.message : "Unknown error"
+      }, 500);
+    }
+  });
+
+  return liveRoutes;
+}

Неке датотеке нису приказане због велике количине промена