pages_rtc.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack,AliRtcSdkChannelProfile } from 'aliyun-rtc-sdk';
  3. import { ToastContainer, toast } from 'react-toastify';
  4. // 辅助函数
  5. function hex(buffer: ArrayBuffer): string {
  6. const hexCodes = [];
  7. const view = new DataView(buffer);
  8. for (let i = 0; i < view.byteLength; i += 4) {
  9. const value = view.getUint32(i);
  10. const stringValue = value.toString(16);
  11. const padding = '00000000';
  12. const paddedValue = (padding + stringValue).slice(-padding.length);
  13. hexCodes.push(paddedValue);
  14. }
  15. return hexCodes.join('');
  16. }
  17. async function generateToken(
  18. appId: string,
  19. appKey: string,
  20. channelId: string,
  21. userId: string,
  22. timestamp: number
  23. ): Promise<string> {
  24. const encoder = new TextEncoder();
  25. const data = encoder.encode(`${appId}${appKey}${channelId}${userId}${timestamp}`);
  26. const hash = await crypto.subtle.digest('SHA-256', data);
  27. return hex(hash);
  28. }
  29. function showToast(type: 'info' | 'success' | 'error', message: string): void {
  30. switch(type) {
  31. case 'info':
  32. toast.info(message);
  33. break;
  34. case 'success':
  35. toast.success(message);
  36. break;
  37. case 'error':
  38. toast.error(message);
  39. break;
  40. }
  41. }
  42. const appId = 'a5842c2a-d94a-43be-81de-1fdb712476e1';
  43. const appKey = 'b71d65f4f84c450f6f058f4ad507bd42';
  44. export const RTCPage = () => {
  45. const [channelId, setChannelId] = useState<string>('');
  46. const [userId, setUserId] = useState<string>('');
  47. const [isJoined, setIsJoined] = useState<boolean>(false);
  48. const aliRtcEngine = useRef<AliRtcEngine | null>(null);
  49. const remoteVideoElMap = useRef<Record<string, HTMLVideoElement>>({});
  50. const remoteVideoContainer = useRef<HTMLDivElement>(null);
  51. function removeRemoteVideo(userId: string, type: 'camera' | 'screen' = 'camera') {
  52. const vid = `${type}_${userId}`;
  53. const el = remoteVideoElMap.current[vid];
  54. if (el) {
  55. aliRtcEngine.current!.setRemoteViewConfig(null, userId, type === 'camera' ? AliRtcVideoTrack.AliRtcVideoTrackCamera : AliRtcVideoTrack.AliRtcVideoTrackScreen);
  56. el.pause();
  57. remoteVideoContainer.current?.removeChild(el);
  58. delete remoteVideoElMap.current[vid];
  59. }
  60. }
  61. function listenEvents() {
  62. if (!aliRtcEngine.current) {
  63. return;
  64. }
  65. aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string, elapsed: number) => {
  66. console.log(`用户 ${userId} 加入频道,耗时 ${elapsed} 秒`);
  67. showToast('info', `用户 ${userId} 上线`);
  68. });
  69. aliRtcEngine.current.on('remoteUserOffLineNotify', (userId, reason) => {
  70. console.log(`用户 ${userId} 离开频道,原因码: ${reason}`);
  71. showToast('info', `用户 ${userId} 下线`);
  72. removeRemoteVideo(userId, 'camera');
  73. removeRemoteVideo(userId, 'screen');
  74. });
  75. aliRtcEngine.current.on('bye', (code) => {
  76. console.log(`bye, code=${code}`);
  77. showToast('info', `您已离开频道,原因码: ${code}`);
  78. });
  79. aliRtcEngine.current.on('videoSubscribeStateChanged', (
  80. userId: string,
  81. oldState: AliRtcSubscribeState,
  82. newState: AliRtcSubscribeState,
  83. interval: number,
  84. channelId: string
  85. ) => {
  86. console.log(`频道 ${channelId} 远端用户 ${userId} 订阅状态由 ${oldState} 变为 ${newState}`);
  87. const vid = `camera_${userId}`;
  88. if (newState === 3) {
  89. const video = document.createElement('video');
  90. video.autoplay = true;
  91. video.className = 'w-80 h-45 mr-2 mb-2 bg-black';
  92. remoteVideoElMap.current[vid] = video;
  93. remoteVideoContainer.current?.appendChild(video);
  94. aliRtcEngine.current!.setRemoteViewConfig(video, userId, AliRtcVideoTrack.AliRtcVideoTrackCamera);
  95. } else if (newState === 1) {
  96. removeRemoteVideo(userId, 'camera');
  97. }
  98. });
  99. aliRtcEngine.current.on('screenShareSubscribeStateChanged', (
  100. userId: string,
  101. oldState: AliRtcSubscribeState,
  102. newState: AliRtcSubscribeState,
  103. interval: number,
  104. channelId: string
  105. ) => {
  106. console.log(`频道 ${channelId} 远端用户 ${userId} 屏幕流的订阅状态由 ${oldState} 变为 ${newState}`);
  107. const vid = `screen_${userId}`;
  108. if (newState === 3) {
  109. const video = document.createElement('video');
  110. video.autoplay = true;
  111. video.className = 'w-80 h-45 mr-2 mb-2 bg-black';
  112. remoteVideoElMap.current[vid] = video;
  113. remoteVideoContainer.current?.appendChild(video);
  114. aliRtcEngine.current!.setRemoteViewConfig(video, userId, AliRtcVideoTrack.AliRtcVideoTrackScreen);
  115. } else if (newState === 1) {
  116. removeRemoteVideo(userId, 'screen');
  117. }
  118. });
  119. }
  120. const handleLoginSubmit = async (e: React.FormEvent) => {
  121. e.preventDefault();
  122. const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
  123. if (!channelId || !userId) {
  124. showToast('error', '数据不完整');
  125. return;
  126. }
  127. const engine = AliRtcEngine.getInstance();
  128. aliRtcEngine.current = engine;
  129. listenEvents();
  130. try {
  131. const token = await generateToken(appId, appKey, channelId, userId, timestamp);
  132. aliRtcEngine.current!.setChannelProfile(AliRtcSdkChannelProfile.AliRtcSdkCommunication);
  133. await aliRtcEngine.current.joinChannel(
  134. {
  135. channelId,
  136. userId,
  137. appId,
  138. token,
  139. timestamp,
  140. },
  141. userId
  142. );
  143. showToast('success', '加入频道成功');
  144. setIsJoined(true);
  145. aliRtcEngine.current!.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
  146. } catch (error) {
  147. console.log('加入频道失败', error);
  148. showToast('error', '加入频道失败');
  149. }
  150. };
  151. const handleLeaveClick = async () => {
  152. Object.keys(remoteVideoElMap.current).forEach(vid => {
  153. const arr = vid.split('_');
  154. removeRemoteVideo(arr[1], arr[0] as 'screen' | 'camera');
  155. });
  156. if (aliRtcEngine.current) {
  157. await aliRtcEngine.current.stopPreview();
  158. await aliRtcEngine.current.leaveChannel();
  159. aliRtcEngine.current.destroy();
  160. aliRtcEngine.current = null;
  161. }
  162. setIsJoined(false);
  163. showToast('info', '已离开频道');
  164. };
  165. useEffect(() => {
  166. AliRtcEngine.setLogLevel(0);
  167. }, []);
  168. return (
  169. <div className="container p-2">
  170. <h1 className="text-2xl font-bold mb-4">aliyun-rtc-sdk 快速开始</h1>
  171. <ToastContainer
  172. position="top-right"
  173. autoClose={5000}
  174. hideProgressBar={false}
  175. newestOnTop={false}
  176. closeOnClick
  177. rtl={false}
  178. pauseOnFocusLoss
  179. draggable
  180. pauseOnHover
  181. />
  182. <div className="flex flex-wrap -mx-2 mt-6">
  183. <div className="w-full md:w-1/2 px-2 mb-4">
  184. <form id="loginForm" onSubmit={handleLoginSubmit}>
  185. <div className="mb-2">
  186. <label htmlFor="channelId" className="block text-gray-700 text-sm font-bold mb-2">频道号</label>
  187. <input
  188. className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
  189. id="channelId"
  190. type="text"
  191. value={channelId}
  192. onChange={(e) => setChannelId(e.target.value)}
  193. />
  194. </div>
  195. <div className="mb-2">
  196. <label htmlFor="userId" className="block text-gray-700 text-sm font-bold mb-2">用户ID</label>
  197. <input
  198. className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
  199. id="userId"
  200. type="text"
  201. value={userId}
  202. onChange={(e) => setUserId(e.target.value)}
  203. />
  204. </div>
  205. <button
  206. id="joinBtn"
  207. type="submit"
  208. className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-2"
  209. disabled={isJoined}
  210. >
  211. 加入频道
  212. </button>
  213. <button
  214. id="leaveBtn"
  215. type="button"
  216. className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-2"
  217. disabled={!isJoined}
  218. onClick={handleLeaveClick}
  219. >
  220. 离开频道
  221. </button>
  222. </form>
  223. <div className="mt-6">
  224. <h4 className="text-lg font-bold mb-2">本地预览</h4>
  225. <video
  226. id="localPreviewer"
  227. muted
  228. className="w-80 h-45 mr-2 mb-2 bg-black"
  229. ></video>
  230. </div>
  231. </div>
  232. <div className="w-full md:w-1/2 px-2">
  233. <h4 className="text-lg font-bold mb-2">远端用户</h4>
  234. <div id="remoteVideoContainer" ref={remoteVideoContainer}></div>
  235. </div>
  236. </div>
  237. </div>
  238. );
  239. };