Browse Source

[demo] - 增加 react typescript 版本的 demo

kevin.song 5 years ago
parent
commit
e7ed023184

+ 26 - 0
examples/react-ts/.gitignore

@@ -0,0 +1,26 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+src/config/index.ts
+yarn.lock

+ 39 - 0
examples/react-ts/README.md

@@ -0,0 +1,39 @@
+# URTC-demo(React Typescript 版本)
+
+## 运行步骤
+
+1. 添加配置
+
+src/config 目录创建 index.ts 文件,并配置 AppId 和 AppKey,示例代码:
+
+```
+const config = {
+  AppId: 'urtc-xxxxxxxx',
+  AppKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+}
+
+export default config;
+```
+
+> 注:
+> 
+> 1. AppId 和 AppKey 可从 URTC 产品中获取
+> 2. AppKey 不可暴露于公网,建议生产环境时,由后端进行保存并由前端调 API 获取
+2. 安装 npm 依赖包
+
+```
+npm install
+```
+
+3. 执行运行命令   
+
+在本地demo目录下,执行以下操作:    
+
+
+```
+npm start
+```
+
+4. 打开页面
+
+浏览器打开 http://localhost:3000

+ 45 - 0
examples/react-ts/package.json

@@ -0,0 +1,45 @@
+{
+  "name": "urtc-demo-react-ts",
+  "version": "1.0.0",
+  "description": "UCloud RTC react typescript 版本的 demo",
+  "dependencies": {
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1",
+    "styled-components": "^5.1.1",
+    "urtc-sdk": "latest"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": "react-app"
+  },
+  "license": "MIT",
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^4.2.4",
+    "@testing-library/react": "^9.3.2",
+    "@testing-library/user-event": "^7.1.2",
+    "@types/jest": "^24.0.0",
+    "@types/node": "^12.0.0",
+    "@types/react": "^16.9.0",
+    "@types/react-dom": "^16.9.0",
+    "@types/styled-components": "^5.1.2",
+    "react-scripts": "3.4.3",
+    "typescript": "~3.7.2"
+  }
+}

BIN
examples/react-ts/public/favicon.ico


+ 43 - 0
examples/react-ts/public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
examples/react-ts/public/logo192.png


BIN
examples/react-ts/public/logo512.png


+ 25 - 0
examples/react-ts/public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
examples/react-ts/public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 38 - 0
examples/react-ts/src/App.css

@@ -0,0 +1,38 @@
+.App {
+  text-align: center;
+}
+
+.App-logo {
+  height: 40vmin;
+  pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.App-header {
+  background-color: #282c34;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+}
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 9 - 0
examples/react-ts/src/App.test.tsx

@@ -0,0 +1,9 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+  const { getByText } = render(<App />);
+  const linkElement = getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});

+ 161 - 0
examples/react-ts/src/App.tsx

@@ -0,0 +1,161 @@
+import React from 'react';
+import sdk, { Client, User, Stream } from 'urtc-sdk';
+import styled from 'styled-components';
+import config from './config';
+import './App.css';
+
+const StreamComponent = styled.div`
+  position: relative;
+  width: 400px;
+  height: 300px;
+  text-align: left;
+`;
+
+const roomId = 'test';
+const userId = `${Math.random()}`;
+
+function useClient(appId: string, token: string): Client {
+  const [client] = React.useState<Client>(() => {
+    const client = new Client(appId, token);
+    (window as any).c = client;
+    return client;
+  });
+  return client;
+}
+
+function useRoomStatus(client: Client): {
+  joined: boolean;
+  join: Function;
+  leave: Function;
+} {
+  const [joined, setJoined] = React.useState<boolean>(false);
+  function join(roomId: string, userId: string) {
+    if (joined) return;
+    client.joinRoom(roomId, userId, (users: User[], streams: Stream[]) => {
+      console.log('demo - join ', users, streams);
+      setJoined(true);
+    }, (err: Error) => {
+      console.error('demo - join ', err);
+    });
+  }
+  function leave() {
+    if (!joined) return;
+    client.leaveRoom(undefined, (): void => {
+      setJoined(false);
+      console.log('离开房间');
+    }, (err: Error): void => {
+      console.error('离开房间 ', err);
+    });
+  }
+  return { joined, join, leave };
+}
+
+function useLocalStream(client: Client, joined: boolean): Stream | undefined {
+  const [stream, setStream] = React.useState<Stream>();
+  React.useEffect(() => {
+    if (joined) {
+      // 加入房间立即推流
+      client.publish({audio: true, video: true, screen: false}, (err: any) => {
+        console.error('demo - publish ', err)
+      });
+    } else {
+      setStream(undefined);
+    }
+    function handlePublished(stream: Stream): void {
+      console.log('stream-published ', stream);
+      setStream(stream);
+      client.play({
+        container: stream.sid,
+        streamId: stream.sid,
+      }, (err) => {
+        if (err) {
+          console.error('play publish stream ', err);
+        }
+      });
+    }
+    client.on('stream-published', handlePublished);
+    return function() {
+      client.off('stream-published', handlePublished);
+    }
+  }, [client, joined]);
+  return stream;
+}
+
+function useRemoteStreams(client: Client, joined: boolean): Stream[] {
+  const [remoteStreams, setRemoteStreams] = React.useState<Stream[]>([]);
+  React.useEffect(() => {
+    if (!joined) {
+      setRemoteStreams([]);
+    }
+    function handleStreamAdded(stream: Stream): void {
+      console.log('stream-added ', stream);
+      client.subscribe(stream.sid, (err) => console.error('demo - subscribe ', err));
+      // remoteStreams.push(stream);
+      setRemoteStreams((preStreams: Stream[]): Stream[] => (preStreams.concat(stream)));
+    }
+    function handleSubscribed(stream: Stream): void {
+      console.log('stream-subscribed ', stream);
+      client.play({
+        container: stream.sid,
+        streamId: stream.sid,
+      }, (err) => {
+        if (err) {
+          console.error('play subscribe stream ', err);
+        }
+      });
+    }
+    function handleStreamRemoved(stream: Stream): void {
+      console.log('stream-removed ', stream);
+      // client.subscribe(stream.sid, (err) => console.error('demo - subscribe ', err));
+      setRemoteStreams((preStreams: Stream[]): Stream[] => (preStreams.filter((item) => item.sid !== stream.sid)));
+    }
+    client.on('stream-added', handleStreamAdded);
+    client.on('stream-subscribed', handleSubscribed);
+    client.on('stream-removed', handleStreamRemoved);
+    return function() {
+      client.off('stream-subscribed', handleSubscribed);
+      client.off('stream-added', handleStreamAdded);
+      client.off('stream-removed', handleStreamRemoved);
+    }
+  }, [client, joined]);
+  return remoteStreams;
+}
+
+function URTC() {
+  console.log('demo - start', sdk.version);
+  const token = sdk.generateToken(config.AppId, config.AppKey, roomId, userId);
+
+  const client = useClient(config.AppId, token);
+  const { joined, join, leave } = useRoomStatus(client);
+  const stream = useLocalStream(client, joined);
+  const remoteStreams = useRemoteStreams(client, joined);
+
+  return (
+    <div>
+      <label>
+        房间号:{roomId}({joined ? "已加入" : "未加入"})
+        {joined ? <button onClick={() => leave()}>离开</button> : <button onClick={() => join(roomId, userId)}>加入</button> }
+        </label>
+      <h5>本地流</h5>
+      {
+        stream
+          ? <StreamComponent className="stream" id={stream.sid}></StreamComponent>
+          : null
+      }
+      <h5>远端流</h5>
+      {remoteStreams.map(stream => <StreamComponent className="stream" key={stream.sid} id={stream.sid}></StreamComponent>)}
+    </div>
+  )
+}
+
+function App() {
+  return (
+    <div className="App">
+      <header className="App-header">
+        <URTC></URTC>
+      </header>
+    </div>
+  );
+}
+
+export default App;

+ 15 - 0
examples/react-ts/src/config/README.md

@@ -0,0 +1,15 @@
+本目录中需要创建 index.js 文件,并配置 AppId 和 AppKey,示例代码:
+
+```
+const config = {
+  AppId: 'urtc-xxxxxxxx',
+  AppKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+}
+
+export default config;
+```
+
+> 注:
+> 
+> 1. AppId 和 AppKey 可从 URTC 产品中获取
+> 2. AppKey 不可暴露于公网,建议生产环境时,由后端进行保存并由前端调 API 获取

+ 13 - 0
examples/react-ts/src/index.css

@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}

+ 17 - 0
examples/react-ts/src/index.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import * as serviceWorker from './serviceWorker';
+
+ReactDOM.render(
+  // <React.StrictMode>
+  <App />,
+  // </React.StrictMode>,
+  document.getElementById('root')
+);
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.unregister();

File diff suppressed because it is too large
+ 7 - 0
examples/react-ts/src/logo.svg


+ 1 - 0
examples/react-ts/src/react-app-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="react-scripts" />

+ 149 - 0
examples/react-ts/src/serviceWorker.ts

@@ -0,0 +1,149 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+  window.location.hostname === 'localhost' ||
+    // [::1] is the IPv6 localhost address.
+    window.location.hostname === '[::1]' ||
+    // 127.0.0.0/8 are considered localhost for IPv4.
+    window.location.hostname.match(
+      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+    )
+);
+
+type Config = {
+  onSuccess?: (registration: ServiceWorkerRegistration) => void;
+  onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export function register(config?: Config) {
+  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+    // The URL constructor is available in all browsers that support SW.
+    const publicUrl = new URL(
+      process.env.PUBLIC_URL,
+      window.location.href
+    );
+    if (publicUrl.origin !== window.location.origin) {
+      // Our service worker won't work if PUBLIC_URL is on a different origin
+      // from what our page is served on. This might happen if a CDN is used to
+      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+      return;
+    }
+
+    window.addEventListener('load', () => {
+      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+      if (isLocalhost) {
+        // This is running on localhost. Let's check if a service worker still exists or not.
+        checkValidServiceWorker(swUrl, config);
+
+        // Add some additional logging to localhost, pointing developers to the
+        // service worker/PWA documentation.
+        navigator.serviceWorker.ready.then(() => {
+          console.log(
+            'This web app is being served cache-first by a service ' +
+              'worker. To learn more, visit https://bit.ly/CRA-PWA'
+          );
+        });
+      } else {
+        // Is not localhost. Just register service worker
+        registerValidSW(swUrl, config);
+      }
+    });
+  }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+  navigator.serviceWorker
+    .register(swUrl)
+    .then(registration => {
+      registration.onupdatefound = () => {
+        const installingWorker = registration.installing;
+        if (installingWorker == null) {
+          return;
+        }
+        installingWorker.onstatechange = () => {
+          if (installingWorker.state === 'installed') {
+            if (navigator.serviceWorker.controller) {
+              // At this point, the updated precached content has been fetched,
+              // but the previous service worker will still serve the older
+              // content until all client tabs are closed.
+              console.log(
+                'New content is available and will be used when all ' +
+                  'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+              );
+
+              // Execute callback
+              if (config && config.onUpdate) {
+                config.onUpdate(registration);
+              }
+            } else {
+              // At this point, everything has been precached.
+              // It's the perfect time to display a
+              // "Content is cached for offline use." message.
+              console.log('Content is cached for offline use.');
+
+              // Execute callback
+              if (config && config.onSuccess) {
+                config.onSuccess(registration);
+              }
+            }
+          }
+        };
+      };
+    })
+    .catch(error => {
+      console.error('Error during service worker registration:', error);
+    });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+  // Check if the service worker can be found. If it can't reload the page.
+  fetch(swUrl, {
+    headers: { 'Service-Worker': 'script' }
+  })
+    .then(response => {
+      // Ensure service worker exists, and that we really are getting a JS file.
+      const contentType = response.headers.get('content-type');
+      if (
+        response.status === 404 ||
+        (contentType != null && contentType.indexOf('javascript') === -1)
+      ) {
+        // No service worker found. Probably a different app. Reload the page.
+        navigator.serviceWorker.ready.then(registration => {
+          registration.unregister().then(() => {
+            window.location.reload();
+          });
+        });
+      } else {
+        // Service worker found. Proceed as normal.
+        registerValidSW(swUrl, config);
+      }
+    })
+    .catch(() => {
+      console.log(
+        'No internet connection found. App is running in offline mode.'
+      );
+    });
+}
+
+export function unregister() {
+  if ('serviceWorker' in navigator) {
+    navigator.serviceWorker.ready
+      .then(registration => {
+        registration.unregister();
+      })
+      .catch(error => {
+        console.error(error.message);
+      });
+  }
+}

+ 5 - 0
examples/react-ts/src/setupTests.ts

@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom/extend-expect';

+ 25 - 0
examples/react-ts/tsconfig.json

@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react"
+  },
+  "include": [
+    "src"
+  ]
+}