├── src ├── server-client │ ├── src │ │ ├── api │ │ │ ├── Rec.interface.ts │ │ │ ├── Index.interface.ts │ │ │ ├── index.ts │ │ │ └── recordManagement.ts │ │ ├── shims-vue.d.ts │ │ ├── views │ │ │ ├── VideoList.interface.ts │ │ │ ├── Details.vue │ │ │ └── VideoList.vue │ │ ├── router │ │ │ └── index.ts │ │ ├── App.vue │ │ ├── components │ │ │ ├── MainHeader.vue │ │ │ └── MainFooter.vue │ │ └── main.ts │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── vue.config.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── tsconfig.json │ └── package.json ├── server │ ├── .prettierrc │ ├── nest-cli.json │ ├── src │ │ ├── app.service.ts │ │ ├── app.controller.ts │ │ ├── recordManagement │ │ │ ├── recordManagement.interface.ts │ │ │ ├── recordManagement.dto.ts │ │ │ ├── recordManagement.module.ts │ │ │ ├── recordManagement.entity.ts │ │ │ ├── recordManagement.contoller.ts │ │ │ └── recordManagement.service.ts │ │ ├── lib │ │ │ ├── globalVars.ts │ │ │ ├── globalInterface.ts │ │ │ └── HttpStatusCode.ts │ │ ├── upload │ │ │ ├── upload.module.ts │ │ │ ├── upload.dto.ts │ │ │ ├── upload.controller.spec.ts │ │ │ ├── upload.service.ts │ │ │ └── upload.controller.ts │ │ ├── app.controller.spec.ts │ │ ├── main.ts │ │ └── app.module.ts │ ├── tsconfig.build.json │ ├── README.md │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── .env │ ├── .development.env │ ├── tsconfig.json │ ├── .gitignore │ ├── .eslintrc.js │ └── package.json └── op-rec │ ├── .idea │ ├── .gitignore │ ├── vcs.xml │ ├── prettier.xml │ ├── modules.xml │ └── web.iml │ ├── dev │ ├── favicon-32x32.png │ └── index.html │ ├── README.md │ ├── src │ ├── op-rec.common.js │ ├── i18n │ │ ├── zh.json │ │ └── index.ts │ ├── lib │ │ ├── globalVars.ts │ │ ├── errorStatus.ts │ │ └── index.ts │ ├── actions │ │ ├── errorHandler.ts │ │ ├── events.ts │ │ ├── getSupportedMimeTypes.ts │ │ ├── toggleREC.ts │ │ ├── stopREC.ts │ │ ├── startREC.ts │ │ ├── downloadAndUpload.ts │ │ └── initDom.ts │ ├── types │ │ ├── index.d.ts │ │ └── op-rec.d.ts │ ├── core.ts │ └── assets │ │ └── icons.ts │ ├── .npmignore │ ├── tsconfig.json │ ├── tslint.json │ ├── webpack.config.js │ └── package.json ├── .idea ├── .gitignore ├── vcs.xml ├── modules.xml ├── inspectionProfiles │ └── Project_Default.xml └── operationRecord.iml ├── yarn.lock ├── README.md ├── package.json ├── .gitignore └── README-zh_CN.md /src/server-client/src/api/Rec.interface.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/op-rec/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/op-rec/dev/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liunnn1994/operationRecord/HEAD/src/op-rec/dev/favicon-32x32.png -------------------------------------------------------------------------------- /src/op-rec/README.md: -------------------------------------------------------------------------------- 1 | # 文档 2 | [https://github.com/asdjgfr/operationRecord#readme](https://github.com/asdjgfr/operationRecord#readme) -------------------------------------------------------------------------------- /src/server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | 3 | @Injectable() 4 | export class AppService {} 5 | -------------------------------------------------------------------------------- /src/server-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liunnn1994/operationRecord/HEAD/src/server-client/public/favicon.ico -------------------------------------------------------------------------------- /src/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/server/README.md: -------------------------------------------------------------------------------- 1 | mysql8 失效的問題 2 | https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server -------------------------------------------------------------------------------- /src/server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "@nestjs/common"; 2 | 3 | @Controller() 4 | export class AppController { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/op-rec/src/op-rec.common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | if (process.env.NODE_ENV === "production") { 3 | module.exports = require("./op-rec.min.js"); 4 | } else { 5 | module.exports = require("./op-rec.js"); 6 | } 7 | -------------------------------------------------------------------------------- /src/server-client/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/op-rec/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/op-rec/.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | .idea/ 4 | dev/ 5 | node_modules/ 6 | .gitignore 7 | .npmignore 8 | .prettierrc 9 | .editorconfig 10 | tslint.json 11 | tsconfig.json 12 | note.md 13 | *.log 14 | *.error 15 | webpack.config.js -------------------------------------------------------------------------------- /src/server-client/src/api/Index.interface.ts: -------------------------------------------------------------------------------- 1 | import { Canceler } from "axios"; 2 | 3 | export interface Res { 4 | code: number; 5 | data?: unknown; 6 | message?: string; 7 | error?: string | Error; 8 | cancel?: Canceler; 9 | } 10 | -------------------------------------------------------------------------------- /src/op-rec/src/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "downloadConfirm": "检测到您的url为local,是否下载?", 3 | "startREC": "开始录制", 4 | "stopREC": "停止录制", 5 | "pauseREC": "暂停录制", 6 | "resumeREC": "继续录制", 7 | "uploadFail": "上传失败", 8 | "errorLevel": "错误等级" 9 | } 10 | -------------------------------------------------------------------------------- /src/server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/server-client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: (config) => { 3 | config.plugin("html").tap((args) => { 4 | args[0].title = "管理页面"; 5 | return args; 6 | }); 7 | }, 8 | devServer: { 9 | proxy: "http://localhost:8990", 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/op-rec/.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RecordManagementInterface { 2 | name: string; 3 | mimetype: string; 4 | size: string; 5 | logs: any[]; 6 | path: string; 7 | originalname: string; 8 | encoding: string; 9 | startTime: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/server/.env: -------------------------------------------------------------------------------- 1 | PORT=启动的端口号 2 | DB_TYPE=数据库类型例如mysql 3 | DB_HOST=数据库地址 4 | DB_PORT=数据库端口 5 | DB_USERNAME=数据库用户名 6 | DB_PASSWORD=数据库密码 7 | DB_DATABASE=数据库名称 8 | DB_SYNCHRONIZE=是否合并 true/false 9 | DB_ETRYATTEMPTS=重试连接数据库的次数 10 | DB_RETRYDELAY=两次重试连接的间隔 11 | DB_KEEPCONNECTUINALIVE=如果为true,在应用程序关闭后连接不会关闭 -------------------------------------------------------------------------------- /src/op-rec/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/op-rec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "ESNext", 6 | "target": "ES6", 7 | "allowJs": false, 8 | "strict": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/server/src/lib/globalVars.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | export const repositoryUrl = "https://github.com/asdjgfr/operationRecord"; 3 | export const publicDir = join(__dirname, "..", "public"); 4 | export const uploadDir = join(publicDir, "uploads"); 5 | export const clientDir = join(__dirname, "..", "client"); 6 | -------------------------------------------------------------------------------- /src/server/src/lib/globalInterface.ts: -------------------------------------------------------------------------------- 1 | import HttpStatusCode from "./HttpStatusCode"; 2 | export interface ResInterface { 3 | code: HttpStatusCode; 4 | data: unknown; 5 | message: string; 6 | } 7 | export interface DatabaseResInf { 8 | success: boolean; 9 | message: string; 10 | error?: Error | undefined; 11 | data?: unknown; 12 | } 13 | -------------------------------------------------------------------------------- /src/server-client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/server/.development.env: -------------------------------------------------------------------------------- 1 | PORT=8990 2 | DB_TYPE=mysql 3 | DB_HOST=localhost 4 | DB_PORT=3306 5 | DB_USERNAME=root 6 | DB_PASSWORD=Aa123456 7 | DB_DATABASE=recordManagement 8 | DB_SYNCHRONIZE=true 9 | #重试连接数据库的次数(默认:10) 10 | DB_ETRYATTEMPTS=10 11 | #两次重试连接的间隔(ms)(默认:3000) 12 | DB_RETRYDELAY=3000 13 | #如果为true,在应用程序关闭后连接不会关闭(默认:false) 14 | DB_KEEPCONNECTUINALIVE=false -------------------------------------------------------------------------------- /src/server-client/src/views/VideoList.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LogInterface { 2 | level: number; 3 | content: string; 4 | timestamp: number; 5 | } 6 | export interface TableData { 7 | name: string; 8 | mimetype: string; 9 | size: string; 10 | encoding: string; 11 | } 12 | export interface RecFull extends TableData { 13 | logs: LogInterface[]; 14 | } 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /src/op-rec/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { generalErrors } from "../lib/errorStatus"; 2 | 3 | export default function (lang: string | undefined) { 4 | let langPackage; 5 | try { 6 | langPackage = require(`./${lang ?? "zh"}.json`); 7 | } catch (e) { 8 | console.error(generalErrors.LanguagePackNotFound, e); 9 | langPackage = require(`./zh.json`); 10 | } 11 | return langPackage; 12 | } 13 | -------------------------------------------------------------------------------- /src/op-rec/src/lib/globalVars.ts: -------------------------------------------------------------------------------- 1 | export const opsRecBox = "data-ops-rec-box"; 2 | export const opsRecIcon = "data-ops-rec-icon"; 3 | export const opsRecShow = "data-ops-rec-show"; 4 | export const opsRecSVGType = "data-ops-rec-type"; 5 | export const logLevels = { 6 | emerg: 0, 7 | alert: 1, 8 | crit: 2, 9 | err: 3, 10 | warning: 4, 11 | notice: 5, 12 | info: 6, 13 | debug: 7, 14 | }; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

OpRec

2 | 3 |
Use the powerful API by modern browser to record, play back and save user operations in any interface.
4 | 5 | 6 | English | [简体中文](https://github.com/asdjgfr/operationRecord/blob/master/README-zh_CN.md) 7 | 8 | ## English docs eta soon. 9 | 10 | Please use [Chinese document](https://github.com/asdjgfr/operationRecord/blob/master/README-zh_CN.md) for the time being. -------------------------------------------------------------------------------- /src/server-client/README.md: -------------------------------------------------------------------------------- 1 | # server-client 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /src/server/src/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { UploadController } from "./upload.controller"; 3 | import { UploadService } from "./upload.service"; 4 | import { RecordManagementModule } from "../recordManagement/recordManagement.module"; 5 | 6 | @Module({ 7 | imports: [RecordManagementModule], 8 | controllers: [UploadController], 9 | providers: [UploadService], 10 | }) 11 | export class UploadModule {} 12 | -------------------------------------------------------------------------------- /src/server-client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | plugins: [ 4 | [ 5 | "import", 6 | { 7 | libraryName: "element-plus", 8 | customStyleName: (name) => { 9 | // 由于 customStyleName 在配置中被声明的原因,`style: true` 会被直接忽略掉, 10 | // 如果你需要使用 scss 源文件,把文件结尾的扩展名从 `.css` 替换成 `.scss` 就可以了 11 | return `element-plus/lib/theme-chalk/${name}.css`; 12 | }, 13 | }, 14 | ], 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist", "public"] 17 | } 18 | -------------------------------------------------------------------------------- /src/op-rec/.idea/web.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/operationRecord.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | export class RecordManagementFindByPKDto { 3 | @ApiProperty({ 4 | type: "string", 5 | description: "需要查找的ID", 6 | }) 7 | id: string; 8 | } 9 | export class RecordManagementFindByLimitDto { 10 | @ApiProperty({ 11 | type: "string", 12 | description: "开始位置", 13 | }) 14 | skip: string; 15 | @ApiProperty({ 16 | type: "string", 17 | description: "查询的条数", 18 | }) 19 | take: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/server/src/upload/upload.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | export class UploadDto { 3 | @ApiProperty({ type: String, description: "后缀名" }) 4 | extname: string; 5 | @ApiProperty({ type: String, description: "文件名" }) 6 | filename: string; 7 | @ApiProperty({ type: String, description: "log的集合" }) 8 | logs: string; 9 | @ApiProperty({ type: String, description: "開始時間" }) 10 | startTime: string; 11 | @ApiProperty({ type: "Binary", description: "上传的文件" }) 12 | file: BinaryType; 13 | } 14 | -------------------------------------------------------------------------------- /src/op-rec/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "no-console": false, 5 | "object-literal-sort-keys": false, 6 | "member-access": false, 7 | "ordered-imports": false, 8 | "no-empty": true, 9 | "only-arrow-functions": false, 10 | "no-implicit-dependencies": [true, ["src"]], 11 | "no-submodule-imports": [true, "src"] 12 | }, 13 | "linterOptions": { 14 | "exclude": ["**/*.json", "node_modules", "./src/op-rec.common.ts"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | .development.env 37 | public/uploads/* -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "operation-record", 3 | "version": "2.0.0", 4 | "description": "利用现代浏览器所提供的强大 API 录制,回放并保存任意界面中的用户操作", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/asdjgfr/operationRecord.git" 11 | }, 12 | "author": "liu", 13 | "license": "Apache-2.0", 14 | "bugs": { 15 | "url": "https://github.com/asdjgfr/operationRecord/issues" 16 | }, 17 | "homepage": "https://github.com/asdjgfr/operationRecord#readme", 18 | "devDependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /src/server-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { TypeOrmModule } from "@nestjs/typeorm"; 3 | import { RecordManagementController } from "./recordManagement.contoller"; 4 | import { RecordManagementService } from "./recordManagement.service"; 5 | import { RecordManagement } from "./recordManagement.entity"; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([RecordManagement])], 9 | controllers: [RecordManagementController], 10 | providers: [RecordManagementService], 11 | exports: [RecordManagementService, TypeOrmModule], 12 | }) 13 | export class RecordManagementModule {} 14 | -------------------------------------------------------------------------------- /src/server-client/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | 3 | export const routes = [ 4 | { 5 | path: "/", 6 | name: "VideoList", 7 | title: "列表", 8 | component: () => 9 | import(/* webpackChunkName: "videoList" */ "../views/VideoList.vue"), 10 | }, 11 | { 12 | path: "/details", 13 | name: "Details", 14 | title: "详情", 15 | component: () => 16 | import(/* webpackChunkName: "videoList" */ "../views/Details.vue"), 17 | }, 18 | ]; 19 | 20 | const router = createRouter({ 21 | history: createWebHistory(process.env.BASE_URL), 22 | routes, 23 | }); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /src/server-client/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Res } from "@/api/Index.interface"; 3 | import { ElMessage } from "element-plus"; 4 | 5 | async function Post(url: string, data: any = {}): Promise { 6 | const cancelTokenSource = axios.CancelToken.source(); 7 | try { 8 | const res = ( 9 | await axios.post(url, { ...data, cancelToken: cancelTokenSource.token }) 10 | ).data; 11 | res.cancel = cancelTokenSource.cancel; 12 | if (res.code !== 200) { 13 | ElMessage.error(res.message); 14 | } 15 | return res; 16 | } catch (error) { 17 | return { 18 | code: 500, 19 | error, 20 | }; 21 | } 22 | } 23 | 24 | export const post = Post; 25 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; 2 | 3 | @Entity() 4 | export class RecordManagement { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | // 文件名 8 | @Column() 9 | name: string; 10 | // 開始時間 11 | @Column() 12 | startTime: string; 13 | // mimetype 14 | @Column() 15 | mimetype: string; 16 | // 大小 17 | @Column() 18 | size: string; 19 | // 记录 20 | @Column("json") 21 | logs; 22 | // 路径 23 | @Column() 24 | path: string; 25 | // 字幕名稱 26 | @Column() 27 | subtitle: string; 28 | // 原始名称 29 | @Column() 30 | originalname: string; 31 | // 编码格式 32 | @Column() 33 | encoding: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from "@nestjs/testing"; 2 | import { INestApplication } from "@nestjs/common"; 3 | import * as request from "supertest"; 4 | import { AppModule } from "./../src/app.module"; 5 | 6 | describe("UploadController (e2e)", () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it("/ (GET)", () => { 19 | return request(app.getHttpServer()) 20 | .get("/") 21 | .expect(200) 22 | .expect("Hello World!"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/server/src/upload/upload.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from "@nestjs/testing"; 2 | import { UploadController } from "./upload.controller"; 3 | import { UploadService } from "./upload.service"; 4 | 5 | describe("AppController", () => { 6 | let appController: UploadController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [UploadController], 11 | providers: [UploadService], 12 | }).compile(); 13 | 14 | appController = app.get(UploadController); 15 | }); 16 | 17 | // describe("root", () => { 18 | // it(`should return ${repositoryUrl}`, () => { 19 | // expect(appController.getIndex()).toBe(repositoryUrl); 20 | // }); 21 | // }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/server-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": ["webpack-env"], 16 | "paths": { 17 | "@/*": ["src/*"] 18 | }, 19 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "src/**/*.tsx", 24 | "src/**/*.vue", 25 | "tests/**/*.ts", 26 | "tests/**/*.tsx" 27 | ], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /src/server/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from "@nestjs/testing"; 2 | import { AppController } from "./app.controller"; 3 | import { AppService } from "./app.service"; 4 | import { repositoryUrl } from "./lib/globalVars"; 5 | 6 | describe("AppController", () => { 7 | let appController: AppController; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | 15 | appController = app.get(AppController); 16 | }); 17 | 18 | describe("root", () => { 19 | it(`should return ${repositoryUrl}`, () => { 20 | expect(appController.getIndex()).toBe(repositoryUrl); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/server-client/src/api/recordManagement.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Res } from "@/api/Index.interface"; 3 | import { post } from "@/api/index"; 4 | 5 | export async function getAllRecs(): Promise { 6 | return post("/v1/get-all-records"); 7 | } 8 | export async function getAllRecsCount(): Promise { 9 | return post("/v1/get-all-records-count"); 10 | } 11 | 12 | export async function getRecsByLimit(skip: number, take: number): Promise { 13 | return post("/v1/get-records-by-limit", { skip, take }); 14 | } 15 | 16 | export async function getRecsByIDs(id: number | string): Promise { 17 | return post("/v1/get-record-by-ids", { id }); 18 | } 19 | 20 | export async function removeRecsByIDs(id: number): Promise { 21 | return post("/v1/remove-records-by-ids", { id }); 22 | } 23 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyInterfaces, LoggerItem } from "../types/op-rec"; 2 | import OpRecInterface from "../types/index"; 3 | import { logLevels } from "../lib/globalVars"; 4 | import { isError } from "lodash-es"; 5 | 6 | export const logger = function ( 7 | this: OpRecInterface, 8 | type: CurrencyInterfaces["loggerType"], 9 | e: ErrorEvent | string 10 | ) { 11 | let level: number; 12 | if (typeof type === "string") { 13 | level = logLevels[type]; 14 | } else { 15 | level = type; 16 | } 17 | this.logs.push({ 18 | level, 19 | content: isError(e) ? e.stack : String(e), 20 | timestamp: new Date().getTime(), 21 | } as LoggerItem); 22 | }; 23 | export const errorCollector = function (this: OpRecInterface, e: ErrorEvent) { 24 | this.logger(3, e.error); 25 | }; 26 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/events.ts: -------------------------------------------------------------------------------- 1 | import OpRecInterface from "../types/index"; 2 | import { GlobalTypesInterfaces } from "../types/op-rec"; 3 | 4 | export default function ( 5 | this: OpRecInterface, 6 | type: GlobalTypesInterfaces["eventsType"], 7 | cb: () => void 8 | ) { 9 | const fnName = ("on" + 10 | type.replace(/^\S/, (s) => 11 | s.toUpperCase() 12 | )) as GlobalTypesInterfaces["fnName"]; 13 | this[fnName] = cb as any; 14 | } 15 | 16 | export function getExtname(this: OpRecInterface) { 17 | const { mimeType } = this; 18 | let extname = mimeType?.split(";")[0].split("/")[1] ?? ""; 19 | switch (extname) { 20 | case "x-matroska": 21 | extname = "mkv"; 22 | break; 23 | } 24 | return extname; 25 | } 26 | export function getBlob(this: OpRecInterface) { 27 | return new Blob(this.recordedChunks, { 28 | type: this.mimeType, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/server-client/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 39 | -------------------------------------------------------------------------------- /src/server-client/src/components/MainHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "tsconfig.json", 5 | sourceType: "module", 6 | }, 7 | plugins: ["@typescript-eslint/eslint-plugin"], 8 | extends: [ 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended", 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: [".eslintrc.js"], 19 | rules: { 20 | "@typescript-eslint/interface-name-prefix": "off", 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "prettier/prettier": ["error", { singleQuote: false }], 25 | }, 26 | overrides: [ 27 | { 28 | files: ["*"], 29 | rules: { 30 | quotes: [2, "double"], 31 | }, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/getSupportedMimeTypes.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | const VIDEO_TYPES = ["webm", "ogg", "mp4", "x-matroska"]; 3 | const VIDEO_CODECS = [ 4 | "vp9", 5 | "vp9.0", 6 | "vp8", 7 | "vp8.0", 8 | "avc1", 9 | "av1", 10 | "h265", 11 | "h.265", 12 | "h264", 13 | "h.264", 14 | "opus", 15 | ]; 16 | 17 | const supportedTypes: string[] = []; 18 | VIDEO_TYPES.forEach((videoType) => { 19 | const type = `video/${videoType}`; 20 | VIDEO_CODECS.forEach((codec) => { 21 | const variations = [ 22 | `${type};codecs=${codec}`, 23 | `${type};codecs:${codec}`, 24 | `${type};codecs=${codec.toUpperCase()}`, 25 | `${type};codecs:${codec.toUpperCase()}`, 26 | `${type}`, 27 | ]; 28 | variations.forEach((variation) => { 29 | if (MediaRecorder.isTypeSupported(variation)) 30 | supportedTypes.push(variation); 31 | }); 32 | }); 33 | }); 34 | return supportedTypes; 35 | } 36 | -------------------------------------------------------------------------------- /src/op-rec/src/lib/errorStatus.ts: -------------------------------------------------------------------------------- 1 | export const mediaDevicesErrors = { 2 | AbortError: "发生了与以下任何其他异常不匹配的错误或故障。", 3 | InvalidStateError: 4 | "调用 getDisplayMedia() 的context中的 document 不是完全激活的; 例如,也许它不是最前面的标签。", 5 | NotAllowedError: 6 | "用户拒绝授予访问屏幕区域的权限,或者不允许当前浏览实例访问屏幕共享。", 7 | NotFoundError: "没有可用于捕获的屏幕视频源。", 8 | NotReadableError: 9 | "用户选择了屏幕,窗口,标签或其他屏幕数据源,但发生了硬件或操作系统级别错误或锁定,从而预先占用了共享所选源。", 10 | OverconstrainedError: 11 | "创建流后,由于无法生成兼容的流导致应用指定的 constraints 失效。", 12 | TypeError: 13 | "指定的 constraints 包括调用 getDisplayMedia() 时不允许的constraints。 这些不受支持的constraints是 advanced 的,任何约束又有一个名为 min 或 exact 的成员。", 14 | StreamNotDetected: "未检测到stream。", 15 | RecordingNotInProgress: "录制未进行。", 16 | }; 17 | 18 | export const envErrors = { 19 | NotLocalhostOrHttps: 20 | "由于chrome政策限制,mediaDevices 需要在本机IP(127.0.0.1或localhost)或https下才可使用。", 21 | NotSupportMediaDevices: 22 | "您的设备不支持mediaDevices API。请更新chrome或更换现代浏览器重试。", 23 | LanguagePackNotFound: "语言包未找到!", 24 | }; 25 | export const generalErrors = { 26 | LanguagePackNotFound: "语言包未找到!", 27 | }; 28 | -------------------------------------------------------------------------------- /src/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "./app.module"; 3 | import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; 4 | import { NestExpressApplication } from "@nestjs/platform-express"; 5 | import { publicDir, clientDir } from "./lib/globalVars"; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | app.setGlobalPrefix("v1"); 10 | app.enableCors(process.env.CORS ? JSON.parse(process.env.CORS) : {}); 11 | app.useStaticAssets(publicDir, { 12 | prefix: "/static", 13 | }); 14 | app.useStaticAssets(clientDir, { 15 | prefix: "/", 16 | }); 17 | 18 | const options = new DocumentBuilder() 19 | .setTitle("接口文档") 20 | .setDescription("后端接口") 21 | .setVersion("1.0") 22 | .build(); 23 | const document = SwaggerModule.createDocument(app, options); 24 | SwaggerModule.setup("swagger", app, document); 25 | await app.listen(process.env.PORT); 26 | } 27 | bootstrap().then(() => { 28 | console.log( 29 | new Date().toLocaleString(), 30 | "项目启动成功,端口:", 31 | process.env.PORT, 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /src/server-client/src/components/MainFooter.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/toggleREC.ts: -------------------------------------------------------------------------------- 1 | import { mediaDevicesErrors } from "../lib/errorStatus"; 2 | import { opsRecSVGType, opsRecShow } from "../lib/globalVars"; 3 | import OpRecInterface from "../types/index"; 4 | 5 | export default function (this: OpRecInterface) { 6 | if (this.mediaRecorder === undefined) { 7 | console.warn(mediaDevicesErrors.RecordingNotInProgress); 8 | return "not running"; 9 | } 10 | const { mediaRecorder } = this; 11 | switch (mediaRecorder.state) { 12 | case "recording": 13 | mediaRecorder.pause(); 14 | this.status = "paused"; 15 | if (this.onPauseREC) { 16 | this.onPauseREC(); 17 | } 18 | break; 19 | case "paused": 20 | mediaRecorder.resume(); 21 | this.status = "recording"; 22 | if (this.onResumeREC) { 23 | this.onResumeREC(); 24 | } 25 | break; 26 | } 27 | if (this.status === "paused" || this.status === "recording") { 28 | const status = this.status === "paused" ? "play" : "paused"; 29 | ["paused", "play"].forEach((item) => { 30 | document 31 | .querySelector(`[${opsRecSVGType}="${item}"]`) 32 | ?.setAttribute(opsRecShow, (item === status).toString()); 33 | }); 34 | } 35 | 36 | return mediaRecorder.state; 37 | } 38 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/stopREC.ts: -------------------------------------------------------------------------------- 1 | import { toggleSVGVisible } from "../lib/index"; 2 | import { opsRecShow, opsRecSVGType } from "../lib/globalVars"; 3 | import OpRecInterface from "../types/index"; 4 | 5 | export function reset(this: OpRecInterface) { 6 | this.startTime = 0; 7 | this.mediaRecorder = undefined; 8 | this.stream = undefined; 9 | this.recordedChunks.splice(0); 10 | this.logs.splice(0); 11 | } 12 | 13 | export default function (this: OpRecInterface) { 14 | this._dataavailableCB = () => { 15 | window.removeEventListener("error", this._errorCollector.bind(this)); 16 | this.status = "stop"; 17 | this._dataavailableCB = () => { 18 | /**/ 19 | }; 20 | toggleSVGVisible.call(this, ":scope > div:last-child svg"); 21 | ["paused", "play"].forEach((item) => { 22 | document 23 | .querySelector(`[${opsRecSVGType}="${item}"]`) 24 | ?.setAttribute(opsRecShow, (item !== "paused").toString()); 25 | }); 26 | if (this.onStopREC) { 27 | this.onStopREC(); 28 | } 29 | }; 30 | 31 | // 停止所有track 32 | (this.stream?.getTracks() ?? []).forEach((track: any) => track.stop()); 33 | // 停止mediaRecorder 34 | const { mediaRecorder } = this; 35 | if (mediaRecorder) { 36 | if (mediaRecorder.state === "paused") { 37 | mediaRecorder.resume(); 38 | this.status = "recording"; 39 | } 40 | try { 41 | mediaRecorder.stop(); 42 | } catch (e) { 43 | /**/ 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/op-rec/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 5 | 6 | module.exports = { 7 | mode: "none", 8 | entry: { 9 | "op-rec": "./src/core.ts", 10 | "op-rec.min": "./src/core.ts", 11 | }, 12 | module: { 13 | unknownContextCritical: false, 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: "ts-loader", 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | plugins: [ 23 | new CopyPlugin({ 24 | patterns: [{ from: path.resolve(__dirname, "src/op-rec.common.js") }], 25 | }), 26 | new CleanWebpackPlugin(), 27 | ], 28 | resolve: { 29 | extensions: [".tsx", ".ts", ".js"], 30 | }, 31 | output: { 32 | path: path.resolve(__dirname, "dist"), 33 | filename: "[name].js", 34 | library: "OpRec", 35 | libraryTarget: "umd", 36 | libraryExport: "default", 37 | umdNamedDefine: true, 38 | }, 39 | optimization: { 40 | minimize: true, 41 | minimizer: [ 42 | new TerserPlugin({ 43 | include: /\.min\.js$/, 44 | }), 45 | ], 46 | usedExports: true, 47 | }, 48 | devtool: "source-map", 49 | devServer: { 50 | contentBase: [path.join(__dirname, "dist"), path.join(__dirname, "dev")], 51 | compress: true, 52 | port: 8989, 53 | injectClient: false, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/server/src/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { RecordManagementInterface } from "../recordManagement/recordManagement.interface"; 3 | import { writeFileSync } from "fs-extra"; 4 | import { uploadDir } from "../lib/globalVars"; 5 | import { extname, join } from "path"; 6 | 7 | @Injectable() 8 | export class UploadService { 9 | generateVtt(newRec: RecordManagementInterface) { 10 | const { startTime, name, logs } = newRec; 11 | const st = Number(startTime); 12 | const { formatVttTime } = this; 13 | const header = `WEBVTT - ${name}字幕文件`; 14 | const content = `${header} 15 | ${logs 16 | .map((log: any, index: number) => { 17 | const { timestamp } = log as any; 18 | const nextTamp = logs[index + 1]?.timestamp ?? new Date().getTime(); 19 | return ` 20 | 21 | ${formatVttTime(timestamp - st)} --> ${formatVttTime(nextTamp - st)} 22 | 错误等级:${log.level} 23 | ${log.content}`; 24 | }) 25 | .join("")}`; 26 | 27 | writeFileSync( 28 | join(uploadDir, `${name.replace(extname(name), "")}.vtt`), 29 | content, 30 | ); 31 | } 32 | formatVttTime(timestamp: number) { 33 | return `${Math.floor(timestamp / 3600000) 34 | .toString() 35 | .padStart(2, "0")}:${Math.floor((timestamp / 1000 / 60) % 60000) 36 | .toString() 37 | .padStart(2, "0")}:${Math.floor((timestamp / 1000) % 60) 38 | .toString() 39 | .padStart(2, "0")}.${timestamp.toString().substr(-3)}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/server-client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { 3 | ElButton, 4 | ElContainer, 5 | ElHeader, 6 | ElMain, 7 | ElFooter, 8 | ElLink, 9 | ElDivider, 10 | ElPageHeader, 11 | ElIcon, 12 | ElTable, 13 | ElTableColumn, 14 | ElPagination, 15 | ElMessageBox, 16 | ElMessage, 17 | } from "element-plus"; 18 | import locale from "element-plus/lib/locale"; 19 | import lang from "element-plus/lib/locale/lang/zh-cn"; 20 | import { library } from "@fortawesome/fontawesome-svg-core"; 21 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 22 | import App from "./App.vue"; 23 | import router from "./router"; 24 | import { faGithub } from "@fortawesome/free-brands-svg-icons"; 25 | import { faHome, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; 26 | import "element-plus/lib/theme-chalk/index.css"; 27 | 28 | const app = createApp(App); 29 | 30 | app.use(router); 31 | 32 | [faGithub, faHome, faInfoCircle].forEach((icon) => { 33 | library.add(icon); 34 | }); 35 | 36 | app.component("fa-icon", FontAwesomeIcon); 37 | locale.use(lang); 38 | [ 39 | ElButton, 40 | ElContainer, 41 | ElHeader, 42 | ElMain, 43 | ElFooter, 44 | ElLink, 45 | ElDivider, 46 | ElPageHeader, 47 | ElIcon, 48 | ElTable, 49 | ElTableColumn, 50 | ElPagination, 51 | ].forEach((component: any) => { 52 | app.component(component.name, component); 53 | }); 54 | 55 | [ElMessageBox, ElMessage].forEach((plugin: any) => { 56 | app.use(plugin); 57 | }); 58 | 59 | app.mount("#app"); 60 | -------------------------------------------------------------------------------- /src/op-rec/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "op-rec", 3 | "version": "1.0.11", 4 | "description": "web端录制库", 5 | "main": "dist/op-rec.common.js", 6 | "unpkg": "dist/op-rec.js", 7 | "typings": "src/types/index.d.ts", 8 | "scripts": { 9 | "dev": "webpack serve", 10 | "build": "webpack", 11 | "lint": "tslint -p tsconfig.json", 12 | "patch": "yarn build && npm version patch && npm publish", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/asdjgfr/operationRecord.git" 18 | }, 19 | "keywords": [ 20 | "operation-record", 21 | "op-rec" 22 | ], 23 | "author": "https://www.2077tech.com/", 24 | "license": "Apache License 2.0", 25 | "bugs": { 26 | "url": "https://github.com/asdjgfr/operationRecord/issues" 27 | }, 28 | "homepage": "https://github.com/asdjgfr/operationRecord#readme", 29 | "devDependencies": { 30 | "clean-webpack-plugin": "^3.0.0", 31 | "copy-webpack-plugin": "^7.0.0", 32 | "pre-commit": "^1.2.2", 33 | "prettier": "^2.2.1", 34 | "terser-webpack-plugin": "^5.1.1", 35 | "ts-loader": "^8.0.14", 36 | "tslint": "^6.1.3", 37 | "tslint-config-prettier": "^1.18.0", 38 | "typescript": "^4.1.3", 39 | "webpack": "^5.18.0", 40 | "webpack-cli": "^4.4.0", 41 | "webpack-dev-server": "^3.11.2" 42 | }, 43 | "dependencies": { 44 | "@types/dom-mediacapture-record": "^1.0.7", 45 | "@types/lodash-es": "^4.17.4", 46 | "lodash-es": "^4.17.20" 47 | }, 48 | "pre-commit": [ 49 | "lint" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/op-rec/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for op-rec 2 | // Project: op-rec 3 | // Definitions by: liu https://github.com/asdjgfr/operationRecord 4 | 5 | import { 6 | CurrencyInterfaces, 7 | DomOptions, 8 | FetchConfig, 9 | HotKeys, 10 | IProps, 11 | LoggerItem, 12 | GlobalTypesInterfaces, 13 | } from "./op-rec"; 14 | 15 | export as namespace opRec; 16 | export = OpRecInterface; 17 | 18 | declare class OpRecInterface { 19 | public props?: IProps; 20 | DOM: HTMLElement | undefined; 21 | startTime: number; 22 | status: GlobalTypesInterfaces["status"]; 23 | recordedChunks: any[]; 24 | logs: LoggerItem[]; 25 | mediaRecorder: MediaRecorder | undefined; 26 | stream: MediaStream | undefined; 27 | startREC: () => Promise; 28 | stopREC: () => void; 29 | toggleREC: () => void; 30 | on: (type: GlobalTypesInterfaces["eventsType"], cb: () => void) => void; 31 | _download: () => void; 32 | getSupportedMimeTypes: () => void; 33 | _dataavailableCB: () => void; 34 | _clickDom: (type: string) => void; 35 | url?: "local" | string; 36 | fetchConfig?: FetchConfig; 37 | mediaConstraints?: any; 38 | mimeType?: string; 39 | lang?: string; 40 | hotKeys?: HotKeys; 41 | dom?: DomOptions; 42 | constructor(props?: IProps); 43 | logger: ( 44 | type: CurrencyInterfaces["loggerType"], 45 | e: ErrorEvent | string 46 | ) => void; 47 | onStartREC?: (stream: MediaStream) => {}; 48 | onStopREC?: () => void; 49 | onPauseREC?: () => void; 50 | onResumeREC?: () => void; 51 | _errorCollector: (e: ErrorEvent) => void; 52 | getBlob: () => string | Blob; 53 | getExtname: () => string; 54 | reset: () => void; 55 | _upload: () => void; 56 | } 57 | 58 | declare namespace OpRecInterface {} 59 | -------------------------------------------------------------------------------- /src/server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { AppController } from "./app.controller"; 3 | import { AppService } from "./app.service"; 4 | import { ConfigModule } from "@nestjs/config"; 5 | import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm"; 6 | import { UploadController } from "./upload/upload.controller"; 7 | import { UploadService } from "./upload/upload.service"; 8 | import { RecordManagementController } from "./recordManagement/recordManagement.contoller"; 9 | import { RecordManagementService } from "./recordManagement/recordManagement.service"; 10 | import { RecordManagementModule } from "./recordManagement/recordManagement.module"; 11 | 12 | const { env } = process; 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot( 16 | Object.assign( 17 | { isGlobal: true }, 18 | env.NODE_ENV === "development" 19 | ? { envFilePath: ".development.env" } 20 | : {}, 21 | ), 22 | ), 23 | TypeOrmModule.forRoot({ 24 | type: env.DB_TYPE, 25 | host: env.DB_HOST, 26 | port: Number(env.DB_PORT), 27 | username: env.DB_USERNAME, 28 | password: env.DB_PASSWORD, 29 | database: env.DB_DATABASE, 30 | entities: [], 31 | synchronize: env.DB_SYNCHRONIZE === "true", 32 | retryAttempts: Number(env.DB_ETRYATTEMPTS), 33 | retryDelay: Number(env.DB_RETRYDELAY), 34 | keepConnectionAlive: env.DB_KEEPCONNECTUINALIVE === "true", 35 | autoLoadEntities: true, 36 | } as TypeOrmModuleOptions), 37 | RecordManagementModule, 38 | ], 39 | controllers: [AppController, UploadController, RecordManagementController], 40 | providers: [AppService, UploadService, RecordManagementService], 41 | }) 42 | export class AppModule {} 43 | -------------------------------------------------------------------------------- /src/server-client/src/views/Details.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 56 | 57 | 71 | -------------------------------------------------------------------------------- /src/server-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "vue-cli-service build", 7 | "lint": "vue-cli-service lint", 8 | "dev": "vue-cli-service serve" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^1.2.34", 12 | "@fortawesome/free-brands-svg-icons": "^5.15.2", 13 | "@fortawesome/free-solid-svg-icons": "^5.15.2", 14 | "@fortawesome/vue-fontawesome": "^3.0.0-3", 15 | "axios": "^0.21.1", 16 | "core-js": "^3.8.3", 17 | "element-plus": "^1.0.2-beta.32", 18 | "vue": "^3.0.0", 19 | "vue-class-component": "^8.0.0-0", 20 | "vue-router": "^4.0.0-0" 21 | }, 22 | "devDependencies": { 23 | "@typescript-eslint/eslint-plugin": "^2.33.0", 24 | "@typescript-eslint/parser": "^2.33.0", 25 | "@vue/cli-plugin-babel": "~4.5.11", 26 | "@vue/cli-plugin-eslint": "~4.5.11", 27 | "@vue/cli-plugin-router": "~4.5.0", 28 | "@vue/cli-plugin-typescript": "~4.5.0", 29 | "@vue/cli-service": "~4.5.11", 30 | "@vue/compiler-sfc": "^3.0.5", 31 | "@vue/eslint-config-typescript": "^5.0.2", 32 | "babel-eslint": "^10.1.0", 33 | "babel-plugin-import": "^1.13.3", 34 | "eslint": "^7.20.0", 35 | "eslint-plugin-vue": "^7.6.0", 36 | "prettier": "2.2.1", 37 | "typescript": "~3.9.3" 38 | }, 39 | "eslintConfig": { 40 | "root": true, 41 | "env": { 42 | "node": true 43 | }, 44 | "extends": [ 45 | "plugin:vue/vue3-essential", 46 | "eslint:recommended", 47 | "@vue/typescript" 48 | ], 49 | "parserOptions": { 50 | "parser": "@typescript-eslint/parser" 51 | }, 52 | "rules": {} 53 | }, 54 | "browserslist": [ 55 | "> 1%", 56 | "last 2 versions", 57 | "not dead" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/server/src/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Post, 5 | UploadedFile, 6 | UseInterceptors, 7 | } from "@nestjs/common"; 8 | import { FileInterceptor } from "@nestjs/platform-express"; 9 | import { UploadService } from "./upload.service"; 10 | import { RecordManagementService } from "../recordManagement/recordManagement.service"; 11 | import { UploadDto } from "./upload.dto"; 12 | import { diskStorage } from "multer"; 13 | import HttpStatusCode from "../lib/HttpStatusCode"; 14 | import { ResInterface } from "../lib/globalInterface"; 15 | import { uploadDir } from "../lib/globalVars"; 16 | import { RecordManagementInterface } from "../recordManagement/recordManagement.interface"; 17 | 18 | @Controller() 19 | export class UploadController { 20 | constructor( 21 | private readonly uploadService: UploadService, 22 | private readonly recordManagementService: RecordManagementService, 23 | ) {} 24 | 25 | @Post("/upload") 26 | @UseInterceptors( 27 | FileInterceptor("file", { 28 | storage: diskStorage({ 29 | destination: uploadDir, 30 | filename(req, file, cb) { 31 | const { body } = req; 32 | return cb(null, `${body.filename}.${body.extname}`); 33 | }, 34 | }), 35 | }), 36 | ) 37 | async uploadFile( 38 | @Body() body: UploadDto, 39 | @UploadedFile() file, 40 | ): Promise { 41 | const { logs } = body; 42 | const newRec: RecordManagementInterface = { 43 | name: file.filename, 44 | path: file.destination, 45 | mimetype: file.mimetype, 46 | size: String(file.size), 47 | logs: Array.isArray(JSON.parse(logs)) ? JSON.parse(logs) : [], 48 | originalname: file.originalname, 49 | encoding: file.encoding, 50 | startTime: body.startTime, 51 | }; 52 | const item = await this.recordManagementService.create(newRec); 53 | this.uploadService.generateVtt(newRec); 54 | return { 55 | code: item.success 56 | ? HttpStatusCode.OK 57 | : HttpStatusCode.INTERNAL_SERVER_ERROR, 58 | data: item.data ?? file, 59 | message: item.error 60 | ? `${item.message}:${String(item.error)}` 61 | : item.message, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | *.local 107 | build/ 108 | 109 | # OS 110 | .DS_Store 111 | 112 | -------------------------------------------------------------------------------- /src/op-rec/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { envErrors } from "./errorStatus"; 2 | import { isArray } from "lodash-es"; 3 | import { InsertRule } from "../types/op-rec"; 4 | import { opsRecShow } from "./globalVars"; 5 | import OpRecInterface from "../types/index"; 6 | 7 | // 检查运行环境 8 | export function checkEnv() { 9 | let res = ""; 10 | if (!navigator.mediaDevices) { 11 | if ( 12 | !/localhost|127.0.0.1/.test(location.hostname ?? "") || 13 | location.protocol !== "https" 14 | ) { 15 | res = envErrors.NotLocalhostOrHttps; 16 | } else { 17 | res = envErrors.NotSupportMediaDevices; 18 | } 19 | } 20 | return res; 21 | } 22 | 23 | export function objectToCssString(object: object) { 24 | return Object.entries(object) 25 | .map(([k, v]) => `${k}:${v}`) 26 | .join(";"); 27 | } 28 | 29 | export function insertRule( 30 | this: OpRecInterface, 31 | rules: InsertRule | InsertRule[] 32 | ) { 33 | let styleSheet = document.querySelector("style"); 34 | if (styleSheet === null) { 35 | styleSheet = document.createElement("style"); 36 | if (this.props?.CSP !== undefined) { 37 | styleSheet.nonce = this.props?.CSP; 38 | } 39 | document.head.appendChild(styleSheet); 40 | } 41 | const r: InsertRule[] = isArray(rules) ? rules : [rules]; 42 | r.forEach((rule: InsertRule) => { 43 | styleSheet?.sheet?.insertRule( 44 | `${rule.selector}{${objectToCssString(rule.style)}}` 45 | ); 46 | }); 47 | } 48 | 49 | export function toggleSVGVisible(this: OpRecInterface, selector: string) { 50 | const doms = this.DOM?.querySelectorAll(selector); 51 | if (doms) { 52 | [...doms].forEach((dom: Element) => { 53 | dom.setAttribute( 54 | opsRecShow, 55 | (dom.getAttribute(opsRecShow) !== "true").toString() 56 | ); 57 | }); 58 | } 59 | } 60 | 61 | export function formatVttTime(timestamp: number) { 62 | return `${Math.floor(timestamp / 3600000) 63 | .toString() 64 | .padStart(2, "0")}:${Math.floor((timestamp / 1000 / 60) % 60000) 65 | .toString() 66 | .padStart(2, "0")}:${Math.floor((timestamp / 1000) % 60) 67 | .toString() 68 | .padStart(2, "0")}.${timestamp.toString().substr(-3)}`; 69 | } 70 | 71 | export function electronVersion() { 72 | return process?.versions?.electron; 73 | } 74 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/startREC.ts: -------------------------------------------------------------------------------- 1 | import { mediaDevicesErrors } from "../lib/errorStatus"; 2 | import { ErrorStatus } from "../types/op-rec"; 3 | import OpRecInterface from "../types/index"; 4 | import i18n from "../i18n/index"; 5 | import { toggleSVGVisible } from "../lib/index"; 6 | import { opsRecShow } from "../lib/globalVars"; 7 | 8 | export default async function (this: OpRecInterface) { 9 | this.stopREC(); 10 | this.reset(); 11 | let isAllow = false; 12 | try { 13 | this.stream = await navigator.mediaDevices.getDisplayMedia( 14 | this.mediaConstraints 15 | ); 16 | isAllow = true; 17 | } catch (e) { 18 | const name = e.name as ErrorStatus["key"]; 19 | const errorMsg = mediaDevicesErrors[name] ?? ""; 20 | console.error(errorMsg, e); 21 | } 22 | if (!isAllow) { 23 | return undefined; 24 | } 25 | const videoTrack = this.stream?.getVideoTracks()[0]; 26 | if (videoTrack) { 27 | videoTrack.addEventListener("ended", () => { 28 | this.stopREC(); 29 | }); 30 | } 31 | 32 | if (!this.stream) { 33 | return undefined; 34 | } 35 | window.addEventListener("error", this._errorCollector.bind(this)); 36 | const mediaRecorder = new MediaRecorder(this.stream, { 37 | mimeType: this.mimeType, 38 | }); 39 | this.mediaRecorder = mediaRecorder; 40 | mediaRecorder.addEventListener("dataavailable", (event: any) => { 41 | if (event.data.size > 0) { 42 | this.recordedChunks.push(event.data); 43 | if (this.url === "local") { 44 | if (confirm(i18n(this.lang ?? "").downloadConfirm)) { 45 | this._download(); 46 | } 47 | } else { 48 | this._upload(); 49 | } 50 | } else { 51 | console.error(mediaDevicesErrors.StreamNotDetected); 52 | } 53 | this._dataavailableCB(); 54 | }); 55 | this.status = "recording"; 56 | this.startTime = new Date().getTime(); 57 | mediaRecorder.start(); 58 | 59 | toggleSVGVisible.call(this, ":scope > div:last-child svg"); 60 | 61 | const doms = this.DOM?.querySelectorAll(":scope > div:first-child svg"); 62 | if (doms) { 63 | [...doms].forEach((dom: Element, index: number) => { 64 | dom.setAttribute(opsRecShow, (!!index).toString()); 65 | }); 66 | } 67 | if (this.onStartREC) { 68 | this.onStartREC(this.stream); 69 | } 70 | return this.stream; 71 | } 72 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.contoller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from "@nestjs/common"; 2 | import { RecordManagementService } from "./recordManagement.service"; 3 | import { ResInterface } from "../lib/globalInterface"; 4 | import HttpStatusCode from "../lib/HttpStatusCode"; 5 | import { 6 | RecordManagementFindByLimitDto, 7 | RecordManagementFindByPKDto, 8 | } from "./recordManagement.dto"; 9 | 10 | @Controller() 11 | export class RecordManagementController { 12 | constructor( 13 | private readonly recordManagementService: RecordManagementService, 14 | ) {} 15 | 16 | @Post("/get-record-by-ids") 17 | async getRecordByID( 18 | @Body() body: RecordManagementFindByPKDto, 19 | ): Promise { 20 | const res = await this.recordManagementService.findByPK(String(body.id)); 21 | return { 22 | code: res.success 23 | ? HttpStatusCode.OK 24 | : HttpStatusCode.INTERNAL_SERVER_ERROR, 25 | data: res.data, 26 | message: res.message, 27 | }; 28 | } 29 | 30 | @Post("/get-all-records") 31 | async getAllRecords(): Promise { 32 | return { 33 | code: HttpStatusCode.OK, 34 | data: await this.recordManagementService.findAll(), 35 | message: "查找成功", 36 | }; 37 | } 38 | 39 | @Post("/get-all-records-count") 40 | async getTotalCount(): Promise { 41 | return { 42 | code: HttpStatusCode.OK, 43 | data: await this.recordManagementService.getTotalCount(), 44 | message: "查找成功", 45 | }; 46 | } 47 | 48 | @Post("/get-records-by-limit") 49 | async getRecordsByLimit( 50 | @Body() body: RecordManagementFindByLimitDto, 51 | ): Promise { 52 | const res = await this.recordManagementService.findByLimit( 53 | body.skip, 54 | body.take, 55 | ); 56 | return { 57 | code: res.success 58 | ? HttpStatusCode.OK 59 | : HttpStatusCode.INTERNAL_SERVER_ERROR, 60 | data: res.data, 61 | message: res.message, 62 | }; 63 | } 64 | 65 | @Post("/remove-records-by-ids") 66 | async removeRecordsByIDs( 67 | @Body() body: RecordManagementFindByPKDto, 68 | ): Promise { 69 | const res = await this.recordManagementService.remove(String(body.id)); 70 | return { 71 | code: res.success 72 | ? HttpStatusCode.OK 73 | : HttpStatusCode.INTERNAL_SERVER_ERROR, 74 | data: body.id, 75 | message: res.message, 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/downloadAndUpload.ts: -------------------------------------------------------------------------------- 1 | import OpRecInterface from "../types/index"; 2 | import { formatVttTime } from "../lib/index"; 3 | import i18n from "../i18n/index"; 4 | 5 | const generateFilename = function (): string { 6 | const now = new Date(); 7 | return `${now.getFullYear()}${ 8 | now.getMonth() + 1 9 | }${now.getDate()}${now.getTime().toString().substr(-4)}`; 10 | }; 11 | const generateVideo = function (this: OpRecInterface, fileName: string) { 12 | const url = URL.createObjectURL(this.getBlob()); 13 | generateDownloadLink.call(this, url, fileName, this.getExtname()); 14 | window.URL.revokeObjectURL(url); 15 | }; 16 | 17 | const generateVtt = function (this: OpRecInterface, fileName: string) { 18 | const { startTime } = this; 19 | const header = `WEBVTT - ${fileName}字幕文件`; 20 | const { logs } = this; 21 | const content = `${header} 22 | ${logs 23 | .map((log, index: number) => { 24 | const { timestamp } = log; 25 | const nextTamp = logs[index + 1]?.timestamp ?? new Date().getTime(); 26 | return ` 27 | 28 | ${formatVttTime(timestamp - startTime)} --> ${formatVttTime( 29 | nextTamp - startTime 30 | )} 31 | ${i18n(this.lang).errorLevel}:${log.level} 32 | ${log.content}`; 33 | }) 34 | .join("")}`; 35 | generateDownloadLink.call( 36 | this, 37 | "data:text/vtt;charset=utf-8," + encodeURIComponent(content), 38 | fileName, 39 | "vtt" 40 | ); 41 | }; 42 | 43 | const generateDownloadLink = function ( 44 | this: OpRecInterface, 45 | url: string, 46 | fileName: string, 47 | extname: string 48 | ) { 49 | const a = document.createElement("a"); 50 | document.body.appendChild(a); 51 | a.style.display = "none"; 52 | a.href = url; 53 | a.download = `${fileName}.${extname}`; 54 | a.click(); 55 | a.parentNode?.removeChild(a); 56 | }; 57 | 58 | export function download(this: OpRecInterface) { 59 | const fileName = generateFilename(); 60 | generateVideo.call(this, fileName); 61 | generateVtt.call(this, fileName); 62 | } 63 | export async function upload(this: OpRecInterface) { 64 | const formData = new FormData(); 65 | formData.append("extname", this.getExtname()); 66 | formData.append("filename", generateFilename()); 67 | formData.append("logs", JSON.stringify(this.logs)); 68 | formData.append("startTime", String(this.startTime)); 69 | formData.append("file", this.getBlob()); 70 | 71 | try { 72 | await fetch(`${(this.url as string).replace(/\/+$/, "")}/v1/upload`, { 73 | method: "POST", 74 | body: formData, 75 | }); 76 | } catch (e) { 77 | console.error(`${i18n(this.lang).uploadFail}:`, e); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "cross-env NODE_ENV=production nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "cross-env NODE_ENV=development nest start --watch", 14 | "start:dev": "cross-env NODE_ENV=development nest start --watch", 15 | "start:debug": "cross-env NODE_ENV=debug nest start --debug --watch", 16 | "start:prod": "node dist/main", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^8.0.4", 26 | "@nestjs/config": "^1.0.1", 27 | "@nestjs/core": "^8.0.4", 28 | "@nestjs/platform-express": "^8.0.4", 29 | "@nestjs/swagger": "^5.0.8", 30 | "@nestjs/typeorm": "^8.0.1", 31 | "fs-extra": "^10.0.0", 32 | "multer": "^1.4.2", 33 | "mysql": "^2.18.1", 34 | "reflect-metadata": "^0.1.13", 35 | "rimraf": "^3.0.2", 36 | "rxjs": "^7.2.0", 37 | "swagger-ui-express": "^4.1.6", 38 | "typeorm": "^0.2.34" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^8.0.2", 42 | "@nestjs/schematics": "^8.0.2", 43 | "@nestjs/testing": "^8.0.4", 44 | "@types/express": "^4.17.13", 45 | "@types/jest": "^26.0.24", 46 | "@types/node": "^16.4.1", 47 | "@types/supertest": "^2.0.11", 48 | "@typescript-eslint/eslint-plugin": "^4.28.4", 49 | "@typescript-eslint/parser": "^4.28.4", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^7.31.0", 52 | "eslint-config-prettier": "8.3.0", 53 | "eslint-plugin-prettier": "^3.4.0", 54 | "jest": "^27.0.6", 55 | "prettier": "^2.3.2", 56 | "supertest": "^6.1.4", 57 | "ts-jest": "^27.0.4", 58 | "ts-loader": "^9.2.3", 59 | "ts-node": "^10.1.0", 60 | "tsconfig-paths": "^3.10.1", 61 | "typescript": "^4.3.5" 62 | }, 63 | "jest": { 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "ts" 68 | ], 69 | "rootDir": "src", 70 | "testRegex": ".*\\.spec\\.ts$", 71 | "transform": { 72 | "^.+\\.(t|j)s$": "ts-jest" 73 | }, 74 | "collectCoverageFrom": [ 75 | "**/*.(t|j)s" 76 | ], 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/op-rec/src/types/op-rec.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface MediaDevices { 3 | getDisplayMedia(constraints?: MediaStreamConstraints): Promise; 4 | } 5 | interface MediaTrackConstraintSet { 6 | displaySurface?: ConstrainDOMString; 7 | logicalSurface?: ConstrainBoolean; 8 | } 9 | } 10 | export interface FetchConfig { 11 | method?: string; 12 | headers?: typeof Headers; 13 | body?: any; 14 | mode?: "cors" | "no-cors" | "same-origin"; 15 | credentials?: "omit" | "same-origin" | "include" | any; 16 | cache?: 17 | | "default" 18 | | "no-store" 19 | | "reload" 20 | | "no-cache" 21 | | "force-cache" 22 | | "only-if-cached"; 23 | redirect?: "follow" | "error" | "manual"; 24 | referrer?: "no-referrer" | "client" | string; 25 | referrerPolicy?: 26 | | "no-referrer" 27 | | "no-referrer-when-downgrade" 28 | | "origin" 29 | | "origin-when-cross-origin" 30 | | "unsafe-url"; 31 | integrity?: string; 32 | } 33 | 34 | interface HotKeys { 35 | start?: KeyboardEvent["code"] | KeyboardEvent["code"][]; 36 | stop?: KeyboardEvent["code"] | KeyboardEvent["code"][]; 37 | toggleREC?: KeyboardEvent["code"] | KeyboardEvent["code"][]; 38 | } 39 | interface DomOptions { 40 | show?: boolean; 41 | style: any; 42 | } 43 | 44 | export interface IProps { 45 | url?: "local" | string; 46 | fetchConfig?: FetchConfig; 47 | mediaConstraints?: any; 48 | mimeType?: string; 49 | lang?: string; 50 | hotKeys?: HotKeys; 51 | dom?: DomOptions; 52 | onStartREC?: (stream: MediaStream) => {}; 53 | onStopREC?: () => void; 54 | onPauseREC?: () => void; 55 | onResumeREC?: () => void; 56 | CSP?: string; 57 | } 58 | 59 | export interface LoggerItem { 60 | level: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; 61 | content: string; 62 | timestamp: number; 63 | } 64 | 65 | interface MediaDevicesErrorStatus { 66 | key: 67 | | "AbortError" 68 | | "InvalidStateError" 69 | | "NotAllowedError" 70 | | "NotFoundError" 71 | | "NotReadableError" 72 | | "OverconstrainedError" 73 | | "TypeError"; 74 | } 75 | 76 | export interface ErrorStatus { 77 | key: MediaDevicesErrorStatus["key"]; 78 | } 79 | 80 | export interface InsertRule { 81 | selector: string; 82 | style: object; 83 | } 84 | 85 | export interface CurrencyInterfaces { 86 | loggerType: 87 | | "emerg" 88 | | "alert" 89 | | "crit" 90 | | "err" 91 | | "warning" 92 | | "notice" 93 | | "info" 94 | | "debug" 95 | | 0 96 | | 1 97 | | 2 98 | | 3 99 | | 4 100 | | 5 101 | | 6 102 | | 7; 103 | } 104 | 105 | export interface GlobalTypesInterfaces { 106 | fnName: "onStartREC" | "onStopREC" | "onPauseREC" | "onResumeREC"; 107 | status: "recording" | "stop" | "paused" | "inactive"; 108 | eventsType: "startREC" | "stopREC" | "pauseREC" | "resumeREC"; 109 | } 110 | -------------------------------------------------------------------------------- /src/op-rec/src/core.ts: -------------------------------------------------------------------------------- 1 | import { checkEnv } from "./lib/index"; 2 | import { IProps, FetchConfig, GlobalTypesInterfaces } from "./types/op-rec"; 3 | import OpRecInterface from "./types/index"; 4 | import startREC from "./actions/startREC"; 5 | import stopREC from "./actions/stopREC"; 6 | import { reset } from "./actions/stopREC"; 7 | import toggleREC from "./actions/toggleREC"; 8 | import { download, upload } from "./actions/downloadAndUpload"; 9 | import initDom from "./actions/initDom"; 10 | import onEvents from "./actions/events"; 11 | import { getBlob, getExtname } from "./actions/events"; 12 | import getSupportedMimeTypes from "./actions/getSupportedMimeTypes"; 13 | import { clickDom } from "./actions/initDom"; 14 | import { errorCollector, logger } from "./actions/errorHandler"; 15 | import { merge } from "lodash-es"; 16 | 17 | class OpRec implements OpRecInterface { 18 | stream = undefined; 19 | mediaRecorder = undefined; 20 | recordedChunks: any[] = []; 21 | logs = []; 22 | startTime = 0; 23 | status: GlobalTypesInterfaces["status"] = "stop"; 24 | DOM: HTMLElement | undefined; 25 | url: string | undefined; 26 | fetchConfig: FetchConfig | undefined; 27 | mediaConstraints: any; 28 | public props: IProps | undefined; 29 | constructor(props?: IProps) { 30 | this.props = props; 31 | const check = checkEnv(); 32 | if (check !== "") { 33 | console.error(check); 34 | } 35 | const defaultConfig: IProps = { 36 | url: "local", 37 | fetchConfig: {}, 38 | mediaConstraints: { 39 | video: true, 40 | audio: true, 41 | }, 42 | mimeType: getSupportedMimeTypes()[0], 43 | lang: "zh", 44 | dom: { 45 | show: true, 46 | style: { 47 | position: "fixed", 48 | right: "2rem", 49 | bottom: "2rem", 50 | width: "40px", 51 | height: "40px", 52 | zIndex: "1000", 53 | cursor: "pointer", 54 | }, 55 | }, 56 | }; 57 | 58 | merge(this, defaultConfig, props); 59 | // @ts-ignore 60 | initDom.call(this); 61 | } 62 | // @ts-ignore 63 | startREC: () => void = startREC.bind(this); 64 | // @ts-ignore 65 | stopREC: () => void = stopREC.bind(this); 66 | // @ts-ignore 67 | toggleREC: () => void = toggleREC.bind(this); 68 | // @ts-ignore 69 | _download: () => void = download.bind(this); 70 | // @ts-ignore 71 | on = onEvents.bind(this); 72 | getSupportedMimeTypes: () => void = getSupportedMimeTypes.bind(this); 73 | _dataavailableCB() { 74 | // 初始化dataavailable的回调,用来解决事件异步的问题 75 | } 76 | // @ts-ignore 77 | _clickDom = clickDom.bind(this); 78 | // @ts-ignore 79 | logger = logger.bind(this); 80 | // @ts-ignore 81 | _errorCollector = errorCollector.bind(this); 82 | // @ts-ignore 83 | getBlob: () => void = getBlob.bind(this); 84 | // @ts-ignore 85 | getExtname: () => void = getExtname.bind(this); 86 | // @ts-ignore 87 | reset: () => void = reset.bind(this); 88 | // @ts-ignore 89 | _upload: () => void = upload.bind(this); 90 | } 91 | 92 | export default OpRec; 93 | -------------------------------------------------------------------------------- /src/server-client/src/views/VideoList.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 107 | -------------------------------------------------------------------------------- /src/server/src/recordManagement/recordManagement.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { InjectRepository } from "@nestjs/typeorm"; 3 | import { Repository, Connection } from "typeorm"; 4 | import { RecordManagementInterface } from "./recordManagement.interface"; 5 | import { RecordManagement } from "./recordManagement.entity"; 6 | import { DatabaseResInf } from "../lib/globalInterface"; 7 | import { removeSync } from "fs-extra"; 8 | import { join, extname } from "path"; 9 | import { uploadDir } from "../lib/globalVars"; 10 | 11 | @Injectable() 12 | export class RecordManagementService { 13 | constructor( 14 | @InjectRepository(RecordManagement) 15 | private rmRepository: Repository, 16 | private readonly connection: Connection, 17 | ) {} 18 | async create(item: RecordManagementInterface): Promise { 19 | const newItem = new RecordManagement(); 20 | newItem.name = item.name; 21 | newItem.path = item.path; 22 | newItem.mimetype = item.mimetype; 23 | newItem.size = item.size; 24 | newItem.logs = item.logs; 25 | newItem.originalname = item.originalname; 26 | newItem.encoding = item.encoding; 27 | newItem.startTime = item.startTime; 28 | newItem.subtitle = item.name.replace(extname(item.name), "") + ".vtt"; 29 | 30 | try { 31 | await this.connection.manager.save(newItem); 32 | return { success: true, message: "创建成功", data: newItem }; 33 | } catch (e) { 34 | return { success: false, message: "创建失败", data: newItem, error: e }; 35 | } 36 | } 37 | 38 | async findAll(): Promise { 39 | const items = await this.rmRepository.find(); 40 | return items.map((item) => ({ ...item, logs: [], path: "" })); 41 | } 42 | 43 | getTotalCount(): Promise { 44 | return this.rmRepository.count(); 45 | } 46 | 47 | async findByLimit(skip = "0", take = "10"): Promise { 48 | try { 49 | const items = await this.rmRepository 50 | .createQueryBuilder() 51 | .skip(Number(skip)) 52 | .take(Number(take)) 53 | .getMany(); 54 | return { 55 | success: true, 56 | data: items.map((item) => ({ 57 | ...item, 58 | logs: [], 59 | path: "", 60 | size: (Number(item.size) / 1024 / 1024).toFixed(2), 61 | })), 62 | message: "查找成功", 63 | }; 64 | } catch (e) { 65 | return { success: false, data: [], message: `查找失败:${e}` }; 66 | } 67 | } 68 | 69 | async findByPK(id: string): Promise { 70 | const ids = [...new Set(id.split(","))]; 71 | const items = await this.rmRepository.find({ 72 | where: ids.map((id) => ({ id })), 73 | }); 74 | 75 | if (items.length === ids.length) { 76 | return { 77 | success: true, 78 | message: "查找成功", 79 | data: items.map((item) => ({ 80 | ...item, 81 | path: `/static/uploads/${item.name}`, 82 | subtitle: `/static/uploads/${item.subtitle}`, 83 | })), 84 | }; 85 | } else if (items.length === 0) { 86 | return { success: false, message: `ID:${id} 不存在` }; 87 | } else { 88 | return { 89 | success: true, 90 | data: items, 91 | message: `部分查找成功,ID:${ids.filter( 92 | (id) => !items.some((item) => Number(item.id) === Number(id)), 93 | )} 不存在`, 94 | }; 95 | } 96 | } 97 | 98 | async remove(id: string): Promise { 99 | const ids = [...new Set(id.split(","))]; 100 | const items = await this.rmRepository.find({ 101 | where: ids.map((id) => ({ id })), 102 | }); 103 | items.forEach((item) => { 104 | removeSync(join(uploadDir, item.name)); 105 | }); 106 | 107 | try { 108 | await this.rmRepository.delete(ids); 109 | return { success: true, message: `ID:${ids.join(",")}删除成功` }; 110 | } catch (e) { 111 | return { success: false, message: `删除失败:${e}` }; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/op-rec/src/assets/icons.ts: -------------------------------------------------------------------------------- 1 | export const record = [ 2 | { 3 | tag: "path", 4 | class: "record", 5 | attrs: { 6 | d: 7 | "M512.104171 127.921872c106.045982 0 202.091556 42.918413 271.469379 112.504578 69.586165 69.586165 112.504578 165.423398 112.504578 271.469379 0 106.045982-42.918413 202.091556-112.504578 271.46938-69.586165 69.586165-165.423398 112.504578-271.469379 112.504577-106.045982 0-202.091556-42.918413-271.46938-112.504577-69.586165-69.586165-112.504578-165.423398-112.504577-271.46938 0-106.045982 42.918413-202.091556 112.504577-271.469379 69.377823-69.377823 165.423398-112.504578 271.46938-112.504578z", 8 | fill: "#00C9CA", 9 | }, 10 | }, 11 | { 12 | tag: "path", 13 | attrs: { 14 | d: 15 | "M647.73469 376.26531c-34.793082-34.793082-82.711699-56.252289-135.838861-56.252289s-101.045778 21.459207-135.838861 56.252289c-34.793082 34.793082-56.252289 82.711699-56.252288 135.838861s21.459207 101.045778 56.252288 135.838861c34.793082 34.793082 82.711699 56.252289 135.838861 56.252288s101.045778-21.459207 135.838861-56.252288c34.793082-34.793082 56.252289-82.711699 56.252289-135.838861s-21.459207-101.045778-56.252289-135.838861z", 16 | fill: "#424242", 17 | }, 18 | }, 19 | { 20 | tag: "path", 21 | attrs: { 22 | d: 23 | "M613.774975 410.225025c-26.042726-26.042726-62.08586-42.085046-101.879146-42.085045-39.793286 0-75.836419 16.042319-101.879145 42.085045s-42.085046 62.08586-42.085046 101.879146c0 39.793286 16.042319 75.836419 42.085046 101.879145s62.08586 42.085046 101.879145 42.085046c39.793286 0 75.836419-16.042319 101.879146-42.085046s42.085046-62.08586 42.085045-101.879145c0.208342-39.793286-16.042319-75.836419-42.085045-101.879146z", 24 | fill: "#F44336", 25 | }, 26 | }, 27 | ]; 28 | 29 | export const stop = [ 30 | { 31 | tag: "path", 32 | attrs: { 33 | d: 34 | "M190.216073 127.921872h643.567854c17.084028 0 32.709664 7.083622 43.960122 18.334079 11.250458 11.250458 18.334079 26.876094 18.334079 43.960122v643.567854c0 17.084028-7.083622 32.709664-18.334079 43.960122-11.250458 11.250458-26.876094 18.334079-43.960122 18.334079H190.216073c-17.084028 0-32.709664-7.083622-43.960122-18.334079-11.250458-11.250458-18.334079-26.876094-18.334079-43.960122V190.216073c0-17.084028 7.083622-32.709664 18.334079-43.960122 11.250458-11.250458 26.876094-18.334079 43.960122-18.334079z", 35 | fill: "#00A1A2", 36 | }, 37 | }, 38 | { 39 | tag: "path", 40 | attrs: { 41 | d: 42 | "M236.259613 182.92411h551.480774c14.792269 0 28.126144 6.041913 37.709867 15.625636 9.583723 9.583723 15.625636 22.917599 15.625636 37.709867v551.480774c0 14.792269-6.041913 28.126144-15.625636 37.709867-9.583723 9.583723-22.917599 15.625636-37.709867 15.625636H236.259613c-14.792269 0-28.126144-6.041913-37.709867-15.625636-9.583723-9.583723-15.625636-22.917599-15.625636-37.709867V236.259613c0-14.792269 6.041913-28.126144 15.625636-37.709867 9.583723-9.583723 23.125941-15.625636 37.709867-15.625636z", 43 | fill: "#00C9CA", 44 | }, 45 | }, 46 | ]; 47 | export const pause = [ 48 | { 49 | tag: "path", 50 | attrs: { 51 | d: "M752.113937 512.104171v383.973957h-176.04883V512.104171z", 52 | fill: "#00C9CA", 53 | }, 54 | }, 55 | { 56 | tag: "path", 57 | attrs: { 58 | d: "M752.113937 127.921872V512.104171h-176.04883V127.921872z", 59 | fill: "#00A1A2", 60 | }, 61 | }, 62 | { 63 | tag: "path", 64 | attrs: { 65 | d: "M447.934893 512.104171v383.973957h-175.840488V512.104171z", 66 | fill: "#00C9CA", 67 | }, 68 | }, 69 | { 70 | tag: "path", 71 | attrs: { 72 | d: "M447.934893 127.921872V512.104171h-175.840488V127.921872z", 73 | fill: "#00A1A2", 74 | }, 75 | }, 76 | ]; 77 | export const play = [ 78 | { 79 | tag: "path", 80 | attrs: { 81 | d: 82 | "M190.216073 512.104171V140.214039l23.959308 13.958901 297.92879 172.090336 297.928789 171.881994 22.9176 13.333875 1.041709 0.625026z", 83 | fill: "#00C9CA", 84 | }, 85 | }, 86 | { 87 | tag: "path", 88 | attrs: { 89 | d: 90 | "M810.03296 525.85473l-297.928789 172.090336-297.92879 171.881994-23.959308 13.958901V512.104171h643.776196z", 91 | fill: "#00A1A2", 92 | }, 93 | }, 94 | ]; 95 | 96 | export default { 97 | record, 98 | stop, 99 | pause, 100 | play, 101 | }; 102 | -------------------------------------------------------------------------------- /src/op-rec/src/actions/initDom.ts: -------------------------------------------------------------------------------- 1 | import { record, pause, stop, play } from "../assets/icons"; 2 | import { insertRule } from "../lib/index"; 3 | import i18n from "../i18n/index"; 4 | import { 5 | opsRecShow, 6 | opsRecBox, 7 | opsRecIcon, 8 | opsRecSVGType, 9 | } from "../lib/globalVars"; 10 | import OpRecInterface from "../types/index"; 11 | 12 | export const clickDom = function (this: OpRecInterface, type: string) { 13 | const { status } = this; 14 | if (type === "playPause") { 15 | switch (status) { 16 | case "stop": 17 | this.startREC(); 18 | break; 19 | case "recording": 20 | case "paused": 21 | this.toggleREC(); 22 | break; 23 | } 24 | } else if (type === "recordStop") { 25 | switch (status) { 26 | case "stop": 27 | this.startREC(); 28 | break; 29 | case "recording": 30 | this.stopREC(); 31 | break; 32 | } 33 | } 34 | }; 35 | 36 | const createSvg = function ( 37 | dom: any[], 38 | parent: HTMLElement, 39 | index: number, 40 | title: string, 41 | type: string 42 | ) { 43 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 44 | const t = document.createElementNS("http://www.w3.org/2000/svg", "title"); 45 | t.innerHTML = title; 46 | svg.appendChild(t); 47 | svg.setAttribute(opsRecIcon, ""); 48 | svg.setAttribute(opsRecShow, index % 2 === 0 ? "true" : "false"); 49 | svg.setAttribute(opsRecSVGType, type); 50 | 51 | [ 52 | ["viewBox", "0 0 1024 1024"], 53 | ["xmlns", "http://www.w3.org/2000/svg"], 54 | ].forEach((kv: string[]) => { 55 | svg.setAttribute(kv[0], kv[1]); 56 | }); 57 | 58 | dom.forEach((item: any) => { 59 | const d = document.createElementNS("http://www.w3.org/2000/svg", item.tag); 60 | for (const [key, value] of Object.entries(item.attrs)) { 61 | d.setAttribute(key, value); 62 | } 63 | svg.appendChild(d); 64 | }); 65 | parent.appendChild(svg); 66 | }; 67 | 68 | export default function (this: OpRecInterface) { 69 | if (!this.dom?.show) { 70 | // 如果不需要dom直接返回undefined 71 | return undefined; 72 | } 73 | 74 | insertRule.call(this, [ 75 | { 76 | selector: `[${opsRecBox}]`, 77 | style: this.dom.style, 78 | }, 79 | { 80 | selector: `[${opsRecIcon}]`, 81 | style: { 82 | width: "80%", 83 | height: "80%", 84 | }, 85 | }, 86 | { 87 | selector: `[${opsRecShow}="true"]`, 88 | style: { 89 | display: "block", 90 | }, 91 | }, 92 | { 93 | selector: `[${opsRecShow}="false"]`, 94 | style: { 95 | display: "none", 96 | }, 97 | }, 98 | { 99 | selector: `[${opsRecBox}]>div`, 100 | style: { 101 | position: "absolute", 102 | left: 0, 103 | top: 0, 104 | width: "100%", 105 | height: "100%", 106 | "background-color": "#fff", 107 | transition: "all 0.2s ease-in-out , transform 0.2s ease-in-out .2s", 108 | transform: "translate(0)", 109 | "border-radius": "50%", 110 | border: "1px solid transparent", 111 | display: "flex", 112 | "justify-content": "center", 113 | "align-items": "center", 114 | }, 115 | }, 116 | { 117 | selector: `[${opsRecBox}]:hover >div:nth-of-type(1)`, 118 | style: { 119 | transform: "translate(calc(-100% - 5px))", 120 | }, 121 | }, 122 | { 123 | selector: `[${opsRecBox}] >div:hover`, 124 | style: { 125 | "border-color": "#00C9CA", 126 | }, 127 | }, 128 | ]); 129 | 130 | const box = document.createElement("div"); 131 | box.setAttribute(opsRecBox, ""); 132 | const playPauseBox = document.createElement("div"); 133 | const recordStopBox = document.createElement("div"); 134 | [play, pause].forEach((dom: any[], index: number) => { 135 | createSvg( 136 | dom, 137 | playPauseBox, 138 | index, 139 | index ? i18n(this.lang ?? "").pauseREC : i18n(this.lang ?? "").resumeREC, 140 | index ? "paused" : "play" 141 | ); 142 | }); 143 | [record, stop].forEach((dom: any[], index: number) => { 144 | createSvg( 145 | dom, 146 | recordStopBox, 147 | index, 148 | index ? i18n(this.lang ?? "").stopREC : i18n(this.lang ?? "").startREC, 149 | index ? "stop" : "record" 150 | ); 151 | }); 152 | box.appendChild(playPauseBox); 153 | box.appendChild(recordStopBox); 154 | document.body.appendChild(box); 155 | playPauseBox.addEventListener( 156 | "click", 157 | this._clickDom.bind(this, "playPause") 158 | ); 159 | recordStopBox.addEventListener( 160 | "click", 161 | this._clickDom.bind(this, "recordStop") 162 | ); 163 | this.DOM = box; 164 | return box; 165 | } 166 | -------------------------------------------------------------------------------- /src/op-rec/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 测试页面 6 | 7 | 8 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |

OpRec

2 | 3 |

利用现代浏览器所提供的强大 API 录制,回放并保存任意界面中的用户操作

4 | 5 |

6 | star 7 |

8 |

9 | npm 10 | count 11 | top language 12 | gzip 13 | gzip 14 | issues 15 | license 16 |

17 | 18 | [English](https://github.com/asdjgfr/operationRecord) | 简体中文 19 | 20 | - [OpRec](#oprec) 21 | - [简述](#简述) 22 | - [特性](#-特性) 23 | - [支持环境](#-支持环境) 24 | - [安装](#-安装) 25 | - [录制端](#录制端) 26 | - [管理端](#管理端) 27 | - [示例](#-示例) 28 | - [本地开发](#-本地开发) 29 | - [文档](#文档) 30 | - [内置对象](#内置对象) 31 | - [构造函数](#构造函数) 32 | - [配置项](#配置项) 33 | - [实例属性](#实例属性) 34 | - [实例方法](#实例方法) 35 | - [参与共建](#-已知问题) 36 | - [参与共建](#-参与共建) 37 | 38 | ## 简述 39 | 40 | 利用现代浏览器的强大`api` _([MediaDevices.getDisplayMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia))_ 录制并回放用户任意界面(不限于浏览器中)的操作,并提供主动上报的功能以及管理系统。 41 | 42 | ## 🌟 特性 43 | 44 | - 🧱 开发: 45 | - 前端库使用[webpack](https://webpack.js.org/)打包为 umd。 46 | - 后端使用[NestJS](https://docs.nestjs.com/),全链路开发和设计工具体系。 47 | - 后端前端页使用[Vue 3](https://v3.vuejs.org/)+[Element Plus](https://element-plus.org/)更现代。 48 | - 📦 开箱即用。 49 | - 🌀 无依赖。 50 | - 🛡 100% TypeScript 开发,尽量规避愚蠢错误。 51 | - ⚙️ 提供管理系统,并可独立使用。 52 | 53 | ## ✔ 支持环境 54 | 55 | - 所有现代浏览器。 56 | 57 | | [![IE / Edge](https://user-gold-cdn.xitu.io/2019/1/30/1689cda8b4c7fe7a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)](http://godban.github.io/browsers-support-badges/) IE / Edge | [![Firefox](https://user-gold-cdn.xitu.io/2019/1/30/1689cda8b445536a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)](http://godban.github.io/browsers-support-badges/) Firefox | [![Chrome](https://user-gold-cdn.xitu.io/2019/1/30/1689cda8b537a517?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)](http://godban.github.io/browsers-support-badges/) Chrome | [![Safari](https://user-gold-cdn.xitu.io/2019/1/30/1689cda8b3d25b6f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)](http://godban.github.io/browsers-support-badges/) Safari | [![Opera](https://user-gold-cdn.xitu.io/2019/1/30/1689cda8b621d60b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)](http://godban.github.io/browsers-support-badges/) Opera | 58 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 59 | | IE 全版本不支持, Edge 79 及以上 | 66 及以上 | 72 及以上 | 13 及以上 | 60 及以上 | 60 | 61 | ## 📦 安装 62 | 63 | ### 录制端 64 | 65 | > 使用 yarn 66 | 67 | ```shell 68 | $ yarn add op-rec 69 | ``` 70 | 71 | > 使用 npm 72 | 73 | ```shell 74 | $ npm install op-rec --save 75 | ``` 76 | 77 | > 在浏览器中 78 | 79 | ```html 80 | 使用CDN 81 | 82 | 或 83 | 84 | 或 85 | 86 | ``` 87 | 88 | ### 管理端 89 | 90 | 在[Release](https://github.com/asdjgfr/operationRecord/releases)中下载并解压,配置`.env`里面的参数。 91 | 92 | 安装依赖 93 | 94 | ```shell 95 | $ yarn 96 | # 或 97 | $ npm i 98 | ``` 99 | 100 | 启动 101 | 102 | ```shell 103 | $ node main.js 104 | ``` 105 | 106 | ## 🔨 示例 107 | 108 | - vue 109 | 110 | ```vue 111 | 114 | 115 | 132 | ``` 133 | 134 | - react 135 | 136 | ```jsx 137 | import React from "react"; 138 | import ReactDOM from "react-dom"; 139 | import OpRec from "op-rec"; 140 | 141 | class App extends React.Component { 142 | componentDidMount() { 143 | const or = new OpRec(); 144 | or.on("startREC", this.startREC); 145 | } 146 | startRec(stream) { 147 | ReactDOM.findDOMNode(this.refs.video).srcObject = stream; 148 | } 149 | render() { 150 | return ( 151 |
152 | 153 |
154 | ); 155 | } 156 | } 157 | 158 | ReactDOM.render(, document.getElementById("container")); 159 | ``` 160 | 161 | - 原生 162 | 163 | ```html 164 | 165 | 166 | 167 | 168 | 175 | ``` 176 | 177 | ## ⌨ 本地开发 178 | 179 | ```shell 180 | $ git clone clone https://github.com/asdjgfr/operationRecord.git 181 | # 开发前端库 182 | $ cd operationRecord/src/op-rec 183 | $ yarn 184 | $ yarn dev 185 | # 开发服务端 186 | $ cd operationRecord/src/server 187 | $ yarn 188 | $ yarn dev 189 | # 开发服务端前端页面 190 | $ cd operationRecord/src/server-client 191 | $ yarn 192 | $ yarn dev 193 | ``` 194 | 195 | ## 文档 196 | 197 | ### 内置对象 198 | 199 | **日志等级** 200 | 201 | - emerg: 0 202 | - alert: 1 203 | - crit: 2 204 | - err: 3 205 | - warning: 4 206 | - notice: 5 207 | - info: 6 208 | - debug: 7 209 | 210 | **LoggerItem** 211 | 212 | 记录对象 213 | 214 | - level: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 215 | 216 | 等级 217 | 218 | - content: string 219 | 220 | 日志内容 221 | 222 | - timestamp: number 223 | 224 | 生成日志时的时间戳 225 | 226 | ### 构造函数 227 | 228 | **OpRec()** 229 | 230 | 创建一个新的录制实例。 231 | 232 | ### 配置项 233 | 234 | **url** 可选 235 | 236 | 完成后上传的地址,默认为 local,地址为 local 的时候会本地生成并下载。 237 | 238 | **fetchConfig** 可选 239 | 240 | fetch 的配置。 241 | 242 | **mediaConstraints** 可选 243 | 244 | [mediaConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints)配置。 245 | 246 | **mimeType** 可选 247 | 248 | mime 类型,默认会选择最佳类型。 249 | 250 | **lang** 可选 251 | 252 | 语言,默认 zh,暂时只有 zh。 253 | 254 | **hotKeys** 可选,预留,尚未支持 255 | 256 | 快捷键。 257 | 258 | **dom** 可选 259 | 260 | 自动生成的 dom 配置。 261 | 262 | - show: boolean 263 | - style:{key:value} 264 | 265 | dom 为 false 或 dom.show 为 false 的时候将不会生成操作的标签。 266 | 267 | **onStartREC** 可选 268 | 269 | 开始录制时的回调,可使用**OpRec.prototype.on("startREC",cb)**替代。 270 | 271 | **onStopREC** 可选 272 | 273 | 结束录制时的回调,可使用**OpRec.prototype.on("stopREC",cb)**替代。 274 | 275 | **onPauseREC** 可选 276 | 277 | 暂停录制时的回调,可使用**OpRec.prototype.on("pauseREC",cb)**替代。 278 | 279 | **onResumeREC** 可选 280 | 281 | 继续录制时的回调,可使用**OpRec.prototype.on("resumeREC",cb)**替代。 282 | 283 | **CSP** 可选 284 | 285 | 用于设置`style`的`nonce`属性。由于在内部配置`DOM`的时候创建了`inline-style`标签,这与[CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src)产生了冲突,配置此项的与`meta`中的`style-src`配合使用。 286 | 287 | ### 实例属性 288 | 289 | **OpRec.prototype.DOM** (HTMLElement | undefined) 290 | 291 | 用于 ui 操作的 dom,当`DomOptions.show`为`false`的时候返回`undefined`。 292 | 293 | **OpRec.prototype.startTime** (number) 294 | 295 | 录制开始时的时间戳,默认为 0。 296 | 297 | **OpRec.prototype.status** ("recording" | "stop" | "paused" | "inactive") 298 | 299 | 当前状态,默认为 stop。 300 | 301 | **OpRec.prototype.recordedChunks** (Blob[]) 302 | 303 | 包含媒体数据的[`Blob`](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob),默认为 []。 304 | 305 | **OpRec.prototype.logs** (LoggerItem[]) 306 | 307 | 记录集合,默认为 []。 308 | 309 | **OpRec.prototype.logs** (LoggerItem[]) 310 | 311 | 记录集合,默认为 []。 312 | 313 | **OpRec.prototype.mediaRecorder** (MediaRecorder | undefined) 314 | 315 | MediaRecorder 实例。 316 | 317 | **OpRec.prototype.stream** (MediaStream | undefined) 318 | 319 | MediaStream。 320 | 321 | **OpRec.prototype.mimeType** (string | undefined) 322 | 323 | mime 类型。 324 | 325 | ### 实例方法 326 | 327 | **OpRec.prototype.startREC()** 328 | 329 | 开始录制,这是一个异步方法。 330 | 331 | **OpRec.prototype.stopREC()** 332 | 333 | 结束录制。 334 | 335 | **OpRec.prototype.toggleREC()** 336 | 337 | 切换录制状态。 338 | 339 | **OpRec.prototype.on(type,cb)** 340 | 341 | event 事件。 342 | 343 | **OpRec.prototype.logger(loggerLever,ErrorEvent|string)** 344 | 345 | - loggerLever:"emerg" | "alert" | "crit" | "err" | "warning" | "notice" | "info" | "debug" | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 346 | 347 | 主动提交记录,第一个参数为记录等级,第二个参数为日志信息。 348 | 349 | **OpRec.prototype.getSupportedMimeTypes()** 350 | 351 | 获取当前运行环境支持的 Mime 类型。 352 | 353 | **OpRec.prototype.getBlob()** 354 | 355 | 获取录制后的 Blob。 356 | 357 | **OpRec.prototype.getExtname()** 358 | 359 | 获取录制的后缀。 360 | 361 | **OpRec.prototype.reset()** 362 | 363 | 重置状态。 364 | 365 | ## 🤐 已知问题 366 | 367 | 由于`mysql`的库并不支持`mysql 8`新版的加密方式,所以使用`8.x`需要修改默认的加密方式: 368 | 369 | ```mysql 370 | $ ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password' 371 | ``` 372 | 373 | 然后刷新: 374 | 375 | ```mysql 376 | $ flush privileges; 377 | ``` 378 | 379 | 或者切换为`5.x`版本。 380 | 381 | ## 🤝 参与共建 382 | 383 | [![PRs Welcome](https://camo.githubusercontent.com/0ff11ed110cfa69f703ef0dcca3cee6141c0a8ef465e8237221ae245de3deb3d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](http://makeapullrequest.com/) 384 | 欢迎[PR](https://github.com/asdjgfr/operationRecord/pulls) 385 | -------------------------------------------------------------------------------- /src/server/src/lib/HttpStatusCode.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Hypertext Transfer Protocol (HTTP) response status codes. 5 | * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} 6 | */ 7 | enum HttpStatusCode { 8 | /** 9 | * The server has received the request headers and the client should proceed to send the request body 10 | * (in the case of a request for which a body needs to be sent; for example, a POST request). 11 | * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. 12 | * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request 13 | * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. 14 | */ 15 | CONTINUE = 100, 16 | 17 | /** 18 | * The requester has asked the server to switch protocols and the server has agreed to do so. 19 | */ 20 | SWITCHING_PROTOCOLS = 101, 21 | 22 | /** 23 | * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. 24 | * This code indicates that the server has received and is processing the request, but no response is available yet. 25 | * This prevents the client from timing out and assuming the request was lost. 26 | */ 27 | PROCESSING = 102, 28 | 29 | /** 30 | * Standard response for successful HTTP requests. 31 | * The actual response will depend on the request method used. 32 | * In a GET request, the response will contain an entity corresponding to the requested resource. 33 | * In a POST request, the response will contain an entity describing or containing the result of the action. 34 | */ 35 | OK = 200, 36 | 37 | /** 38 | * The request has been fulfilled, resulting in the creation of a new resource. 39 | */ 40 | CREATED = 201, 41 | 42 | /** 43 | * The request has been accepted for processing, but the processing has not been completed. 44 | * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. 45 | */ 46 | ACCEPTED = 202, 47 | 48 | /** 49 | * SINCE HTTP/1.1 50 | * The server is a transforming proxy that received a 200 OK from its origin, 51 | * but is returning a modified version of the origin's response. 52 | */ 53 | NON_AUTHORITATIVE_INFORMATION = 203, 54 | 55 | /** 56 | * The server successfully processed the request and is not returning any content. 57 | */ 58 | NO_CONTENT = 204, 59 | 60 | /** 61 | * The server successfully processed the request, but is not returning any content. 62 | * Unlike a 204 response, this response requires that the requester reset the document view. 63 | */ 64 | RESET_CONTENT = 205, 65 | 66 | /** 67 | * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. 68 | * The range header is used by HTTP clients to enable resuming of interrupted downloads, 69 | * or split a download into multiple simultaneous streams. 70 | */ 71 | PARTIAL_CONTENT = 206, 72 | 73 | /** 74 | * The message body that follows is an XML message and can contain a number of separate response codes, 75 | * depending on how many sub-requests were made. 76 | */ 77 | MULTI_STATUS = 207, 78 | 79 | /** 80 | * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, 81 | * and are not being included again. 82 | */ 83 | ALREADY_REPORTED = 208, 84 | 85 | /** 86 | * The server has fulfilled a request for the resource, 87 | * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. 88 | */ 89 | IM_USED = 226, 90 | 91 | /** 92 | * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). 93 | * For example, this code could be used to present multiple video format options, 94 | * to list files with different filename extensions, or to suggest word-sense disambiguation. 95 | */ 96 | MULTIPLE_CHOICES = 300, 97 | 98 | /** 99 | * This and all future requests should be directed to the given URI. 100 | */ 101 | MOVED_PERMANENTLY = 301, 102 | 103 | /** 104 | * This is an example of industry practice contradicting the standard. 105 | * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect 106 | * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 107 | * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 108 | * to distinguish between the two behaviours. However, some Web applications and frameworks 109 | * use the 302 status code as if it were the 303. 110 | */ 111 | FOUND = 302, 112 | 113 | /** 114 | * SINCE HTTP/1.1 115 | * The response to the request can be found under another URI using a GET method. 116 | * When received in response to a POST (or PUT/DELETE), the client should presume that 117 | * the server has received the data and should issue a redirect with a separate GET message. 118 | */ 119 | SEE_OTHER = 303, 120 | 121 | /** 122 | * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. 123 | * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. 124 | */ 125 | NOT_MODIFIED = 304, 126 | 127 | /** 128 | * SINCE HTTP/1.1 129 | * The requested resource is available only through a proxy, the address for which is provided in the response. 130 | * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. 131 | */ 132 | USE_PROXY = 305, 133 | 134 | /** 135 | * No longer used. Originally meant "Subsequent requests should use the specified proxy." 136 | */ 137 | SWITCH_PROXY = 306, 138 | 139 | /** 140 | * SINCE HTTP/1.1 141 | * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. 142 | * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. 143 | * For example, a POST request should be repeated using another POST request. 144 | */ 145 | TEMPORARY_REDIRECT = 307, 146 | 147 | /** 148 | * The request and all future requests should be repeated using another URI. 149 | * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. 150 | * So, for example, submitting a form to a permanently redirected resource may continue smoothly. 151 | */ 152 | PERMANENT_REDIRECT = 308, 153 | 154 | /** 155 | * The server cannot or will not process the request due to an apparent client error 156 | * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). 157 | */ 158 | BAD_REQUEST = 400, 159 | 160 | /** 161 | * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet 162 | * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the 163 | * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means 164 | * "unauthenticated",i.e. the user does not have the necessary credentials. 165 | */ 166 | UNAUTHORIZED = 401, 167 | 168 | /** 169 | * Reserved for future use. The original intention was that this code might be used as part of some form of digital 170 | * cash or micro payment scheme, but that has not happened, and this code is not usually used. 171 | * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. 172 | */ 173 | PAYMENT_REQUIRED = 402, 174 | 175 | /** 176 | * The request was valid, but the server is refusing action. 177 | * The user might not have the necessary permissions for a resource. 178 | */ 179 | FORBIDDEN = 403, 180 | 181 | /** 182 | * The requested resource could not be found but may be available in the future. 183 | * Subsequent requests by the client are permissible. 184 | */ 185 | NOT_FOUND = 404, 186 | 187 | /** 188 | * A request method is not supported for the requested resource; 189 | * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. 190 | */ 191 | METHOD_NOT_ALLOWED = 405, 192 | 193 | /** 194 | * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. 195 | */ 196 | NOT_ACCEPTABLE = 406, 197 | 198 | /** 199 | * The client must first authenticate itself with the proxy. 200 | */ 201 | PROXY_AUTHENTICATION_REQUIRED = 407, 202 | 203 | /** 204 | * The server timed out waiting for the request. 205 | * According to HTTP specifications: 206 | * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." 207 | */ 208 | REQUEST_TIMEOUT = 408, 209 | 210 | /** 211 | * Indicates that the request could not be processed because of conflict in the request, 212 | * such as an edit conflict between multiple simultaneous updates. 213 | */ 214 | CONFLICT = 409, 215 | 216 | /** 217 | * Indicates that the resource requested is no longer available and will not be available again. 218 | * This should be used when a resource has been intentionally removed and the resource should be purged. 219 | * Upon receiving a 410 status code, the client should not request the resource in the future. 220 | * Clients such as search engines should remove the resource from their indices. 221 | * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. 222 | */ 223 | GONE = 410, 224 | 225 | /** 226 | * The request did not specify the length of its content, which is required by the requested resource. 227 | */ 228 | LENGTH_REQUIRED = 411, 229 | 230 | /** 231 | * The server does not meet one of the preconditions that the requester put on the request. 232 | */ 233 | PRECONDITION_FAILED = 412, 234 | 235 | /** 236 | * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". 237 | */ 238 | PAYLOAD_TOO_LARGE = 413, 239 | 240 | /** 241 | * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, 242 | * in which case it should be converted to a POST request. 243 | * Called "Request-URI Too Long" previously. 244 | */ 245 | URI_TOO_LONG = 414, 246 | 247 | /** 248 | * The request entity has a media type which the server or resource does not support. 249 | * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. 250 | */ 251 | UNSUPPORTED_MEDIA_TYPE = 415, 252 | 253 | /** 254 | * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. 255 | * For example, if the client asked for a part of the file that lies beyond the end of the file. 256 | * Called "Requested Range Not Satisfiable" previously. 257 | */ 258 | RANGE_NOT_SATISFIABLE = 416, 259 | 260 | /** 261 | * The server cannot meet the requirements of the Expect request-header field. 262 | */ 263 | EXPECTATION_FAILED = 417, 264 | 265 | /** 266 | * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, 267 | * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by 268 | * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. 269 | */ 270 | I_AM_A_TEAPOT = 418, 271 | 272 | /** 273 | * The request was directed at a server that is not able to produce a response (for example because a connection reuse). 274 | */ 275 | MISDIRECTED_REQUEST = 421, 276 | 277 | /** 278 | * The request was well-formed but was unable to be followed due to semantic errors. 279 | */ 280 | UNPROCESSABLE_ENTITY = 422, 281 | 282 | /** 283 | * The resource that is being accessed is locked. 284 | */ 285 | LOCKED = 423, 286 | 287 | /** 288 | * The request failed due to failure of a previous request (e.g., a PROPPATCH). 289 | */ 290 | FAILED_DEPENDENCY = 424, 291 | 292 | /** 293 | * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. 294 | */ 295 | UPGRADE_REQUIRED = 426, 296 | 297 | /** 298 | * The origin server requires the request to be conditional. 299 | * Intended to prevent "the 'lost update' problem, where a client 300 | * GETs a resource's state, modifies it, and PUTs it back to the server, 301 | * when meanwhile a third party has modified the state on the server, leading to a conflict." 302 | */ 303 | PRECONDITION_REQUIRED = 428, 304 | 305 | /** 306 | * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. 307 | */ 308 | TOO_MANY_REQUESTS = 429, 309 | 310 | /** 311 | * The server is unwilling to process the request because either an individual header field, 312 | * or all the header fields collectively, are too large. 313 | */ 314 | REQUEST_HEADER_FIELDS_TOO_LARGE = 431, 315 | 316 | /** 317 | * A server operator has received a legal demand to deny access to a resource or to a set of resources 318 | * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. 319 | */ 320 | UNAVAILABLE_FOR_LEGAL_REASONS = 451, 321 | 322 | /** 323 | * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. 324 | */ 325 | INTERNAL_SERVER_ERROR = 500, 326 | 327 | /** 328 | * The server either does not recognize the request method, or it lacks the ability to fulfill the request. 329 | * Usually this implies future availability (e.g., a new feature of a web-service API). 330 | */ 331 | NOT_IMPLEMENTED = 501, 332 | 333 | /** 334 | * The server was acting as a gateway or proxy and received an invalid response from the upstream server. 335 | */ 336 | BAD_GATEWAY = 502, 337 | 338 | /** 339 | * The server is currently unavailable (because it is overloaded or down for maintenance). 340 | * Generally, this is a temporary state. 341 | */ 342 | SERVICE_UNAVAILABLE = 503, 343 | 344 | /** 345 | * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. 346 | */ 347 | GATEWAY_TIMEOUT = 504, 348 | 349 | /** 350 | * The server does not support the HTTP protocol version used in the request 351 | */ 352 | HTTP_VERSION_NOT_SUPPORTED = 505, 353 | 354 | /** 355 | * Transparent content negotiation for the request results in a circular reference. 356 | */ 357 | VARIANT_ALSO_NEGOTIATES = 506, 358 | 359 | /** 360 | * The server is unable to store the representation needed to complete the request. 361 | */ 362 | INSUFFICIENT_STORAGE = 507, 363 | 364 | /** 365 | * The server detected an infinite loop while processing the request. 366 | */ 367 | LOOP_DETECTED = 508, 368 | 369 | /** 370 | * Further extensions to the request are required for the server to fulfill it. 371 | */ 372 | NOT_EXTENDED = 510, 373 | 374 | /** 375 | * The client needs to authenticate to gain network access. 376 | * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used 377 | * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). 378 | */ 379 | NETWORK_AUTHENTICATION_REQUIRED = 511, 380 | } 381 | 382 | export default HttpStatusCode; 383 | --------------------------------------------------------------------------------