├── 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 |
5 |
6 |
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 |
4 |
5 |
6 |
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 |
12 | 对不起 <%= htmlWebpackPlugin.options.title %>
14 | 必须开启JavaScript,请开启后刷新重试。
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
39 |
--------------------------------------------------------------------------------
/src/server-client/src/components/MainHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
18 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 查看
13 | 删除
16 |
17 |
18 |
19 |
28 |
29 |
30 |
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 | webgl 10w个点随机运动
24 |
25 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/README-zh_CN.md:
--------------------------------------------------------------------------------
1 | OpRec
2 |
3 | 利用现代浏览器所提供的强大 API 录制,回放并保存任意界面中的用户操作
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 | | [](http://godban.github.io/browsers-support-badges/) IE / Edge | [](http://godban.github.io/browsers-support-badges/) Firefox | [](http://godban.github.io/browsers-support-badges/) Chrome | [](http://godban.github.io/browsers-support-badges/) Safari | [](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 |
112 |
113 |
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 | [](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 |
--------------------------------------------------------------------------------