docs: 添加博客园文章内容
@@ -1,4 +1,88 @@
|
||||
[
|
||||
{
|
||||
"created-at": "2025-07-02 22:20",
|
||||
"is-hide": "",
|
||||
"slug": "bluefox-nx1-flash-gsi",
|
||||
"title": "蓝狐(BLUEFOX)NX1 刷 GSI 系统",
|
||||
"updated-at": "2025-07-03 21:54"
|
||||
},
|
||||
{
|
||||
"created-at": "2025-04-18 22:55",
|
||||
"is-hide": "",
|
||||
"slug": "ffmpeg-merge-video-files",
|
||||
"title": "使用 FFmpeg 合并多个视频文件",
|
||||
"updated-at": "2025-07-03 21:58"
|
||||
},
|
||||
{
|
||||
"created-at": "2025-03-22 22:20",
|
||||
"is-hide": "",
|
||||
"slug": "fix-redirect-dingtalk-miniprogram-scheme-blank-page",
|
||||
"title": "解决钉钉通过短链接跳转小程序 scheme 的方式打开小程序时会有一个空白页面的问题",
|
||||
"updated-at": "2025-03-22 22:32"
|
||||
},
|
||||
{
|
||||
"created-at": "2025-02-11 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "post-2025-02-11-1",
|
||||
"title": "在用 uni-app 开发钉钉小程序的时候遇到一个奇怪的问题,发送请求拿不到返回的数据",
|
||||
"updated-at": "2025-02-12 09:51"
|
||||
},
|
||||
{
|
||||
"created-at": "2024-01-19 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "fix-ant-tree-select-can-select-disabled-item",
|
||||
"title": "解决 Ant TreeSelect(树选择)组件可以使用键盘选中 disabled(已禁用)项的问题",
|
||||
"updated-at": "2025-02-20 22:56"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-12-18 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "nodejs-check-is-tablet-pc",
|
||||
"title": "Electron 或 Node.js 判断当前设备是否支持触摸屏",
|
||||
"updated-at": "2025-02-20 22:57"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-11-07 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "miui-dial-plate-commands",
|
||||
"title": "MIUI 拨号盘指令(代码)合集",
|
||||
"updated-at": "2025-02-20 22:58"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-11-06 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "calculation-test-code",
|
||||
"title": "运算速度测试代码",
|
||||
"updated-at": "2025-02-20 22:58"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-10-08 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "use-css-function-in-less",
|
||||
"title": "在 Less 中使用与 Less 内置函数同名的原生 CSS 函数",
|
||||
"updated-at": "2025-02-20 22:58"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-09-17 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "common-usage-git-commands",
|
||||
"title": "常用的 Git 命令",
|
||||
"updated-at": "2025-02-20 22:59"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-09-17 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "javascript-define-and-init-array",
|
||||
"title": "JavaScript 创建并初始化任意长度的数组",
|
||||
"updated-at": "2025-02-20 22:59"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-05-30 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "zlm-rtc-client-multi-video-pull-once",
|
||||
"title": "通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次",
|
||||
"updated-at": "2025-02-12 09:50"
|
||||
},
|
||||
{
|
||||
"created-at": "2023-02-21 00:00",
|
||||
"is-hide": "",
|
||||
@@ -433,6 +517,13 @@
|
||||
"title": "百度触屏版首页不同样式的页面",
|
||||
"updated-at": "2025-03-16 22:32"
|
||||
},
|
||||
{
|
||||
"created-at": "2018-08-12 00:00",
|
||||
"is-hide": "",
|
||||
"slug": "windows-copy-command-usage",
|
||||
"title": "Windows copy 命令的妙用(文件里藏文件、合并文件)",
|
||||
"updated-at": "2025-03-29 18:30"
|
||||
},
|
||||
{
|
||||
"created-at": "2018-08-09 00:00",
|
||||
"is-hide": "",
|
||||
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/content/assets/image-20250304100243-rzjrgon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/content/assets/image-20250304100347-3im6ft6.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
docs/content/assets/image-20250702224334-vyg3sj8.png
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
docs/content/assets/image-20250702232252-k8k0r2r.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/content/assets/image-20250702233608-t3r8wve.png
Normal file
After Width: | Height: | Size: 306 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 189 KiB |
After Width: | Height: | Size: 185 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 111 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 6.2 KiB |
77
docs/content/bluefox-nx1-flash-gsi.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: 蓝狐(BLUEFOX)NX1 刷 GSI 系统
|
||||
date: 2025-07-02T22:27:40Z
|
||||
lastmod: 2025-07-03T21:54:29Z
|
||||
tags: [Android,GSI,刷机,系统]
|
||||
---
|
||||
|
||||
# 蓝狐(BLUEFOX)NX1 刷 GSI 系统
|
||||
|
||||
> 主要记录一下操作过程,操作前请确保具备一定的刷机经验。
|
||||
|
||||
## 所需工具和文件
|
||||
|
||||
1. MTK Client(可选,主要用于备份分区)。
|
||||
2. ADB 工具([下载地址](https://developer.android.google.cn/tools/releases/platform-tools?hl=zh-cn))。
|
||||
3. GSI 文件包(例如 [Google GSI](https://developer.android.google.cn/topic/generic-system-image/releases?hl=zh-cn),本文中使用的是 `aosp_arm64-exp-BP1A.250405.005.C1-13151952-61d23231.zip`)。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 一、使用 MTK Client 备份分区
|
||||
|
||||
手机关机,打开 MTK Client 软件,手机同时按住“音量加”和“音量减”键,然后通过数据线连接到电脑。
|
||||
|
||||

|
||||
|
||||
使用 MTK Client 的“Read partition”功能读出除“userdata”(用户数据)以外的分区,备份,以便刷坏了可以还原。
|
||||
|
||||

|
||||
|
||||
### 二、解锁 BootLoader
|
||||
|
||||
> 注意:解锁操作会清除手机数据,恢复出厂设置。
|
||||
|
||||
进入手机设置的“开发人员选项”,打开“USB 调试”和“OEM 解锁”选项的开关。
|
||||
|
||||
将手机通过数据线连接到电脑。
|
||||
|
||||
电脑在 ADB 工具的目录下执行 `adb devices` 命令,此时手机上应该会弹出授权提示窗口,点击“允许”以授权。
|
||||
|
||||
然后执行 `adb reboot bootloader` 命令,等待几秒钟,直到设备重新启动进入引导加载程序。
|
||||
|
||||
> 注意:进入 fastboot 模式后,大概率会因为没有驱动程序,执行 `fastboot` 命令失败,需要参考“[https://www.cnblogs.com/changweijinghu/p/16880803.html](https://www.cnblogs.com/changweijinghu/p/16880803.html)”解决。
|
||||
>
|
||||
> 驱动程序下载地址:[获取 Google USB 驱动程序](https://developer.android.com/studio/run/win-usb) / [usb_driver_r13-windows.zip](https://dl.google.com/android/repository/usb_driver_r13-windows.zip)
|
||||
|
||||
之后执行命令 `fastboot flashing unlock`,手机屏幕上会显示解锁确认:
|
||||
|
||||
```text
|
||||
Press the Volume UP/Down buttons to select Yes or No.
|
||||
Yes (Volume UP): Unlock (may void warranty).
|
||||
No (Volume Down): Do not unlock bootloader.
|
||||
```
|
||||
|
||||
此时按下“音量加”键,确认解锁(**将会清除手机数据,恢复出厂设置**)。
|
||||
|
||||
等待片刻,如无意外,会显示“解锁成功”。
|
||||
|
||||
### 三、刷入 GSI
|
||||
|
||||
将下载的 GSI 文件包解压到 ADB 工具所在文件夹内。
|
||||
|
||||
> 例如本文使用的 `aosp_arm64-exp-BP1A.250405.005.C1-13151952-61d23231.zip`,可以解压得到 `system.img` 和 `vbmeta.img` 这两个镜像文件。
|
||||
|
||||
依次执行以下命令,刷入镜像:
|
||||
|
||||
```text
|
||||
fastboot flash vbmeta vbmeta.img
|
||||
fastboot reboot fastboot
|
||||
fastboot flash system system.img
|
||||
fastboot reboot
|
||||
```
|
||||
|
||||
> 注意:执行 `fastboot reboot fastboot` 命令后如果等待比较久都没出现“Finished”提示,则需要插拔一下数据线。
|
||||
|
||||

|
||||
|
||||
|
67
docs/content/calculation-test-code.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: 运算速度测试代码
|
||||
date: 2025-02-11T20:55:04Z
|
||||
lastmod: 2025-02-20T22:58:29Z
|
||||
tags: [测试,代码片段]
|
||||
---
|
||||
|
||||
# 运算速度测试代码
|
||||
|
||||
#### C
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
int main()
|
||||
{
|
||||
long tStart = time(NULL);
|
||||
long tEnd = tStart + 10;
|
||||
long tNow = 0;
|
||||
long count = 0;
|
||||
do {
|
||||
double num = sqrt(count);
|
||||
tNow = time(NULL);
|
||||
count++;
|
||||
} while (tNow < tEnd);
|
||||
printf("%ld", count);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### Java
|
||||
|
||||
```java
|
||||
public class CalcTest {
|
||||
public static void main(String[] args) {
|
||||
long dMillis = 2000;
|
||||
long dSeconds = 0;
|
||||
long tStart = System.currentTimeMillis();
|
||||
long tEnd = 0;
|
||||
long tCurrent = 0;
|
||||
long count = 0;
|
||||
|
||||
if (args.length == 1) {
|
||||
try {
|
||||
dMillis = Long.parseLong(args[0]);
|
||||
} catch (Exception err) {
|
||||
System.out.println("[Error] invalid duration: " + args[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dSeconds = dMillis / 1000;
|
||||
tEnd = tStart + dMillis;
|
||||
|
||||
System.out.println("[Set] duration = " + dSeconds + "s");
|
||||
|
||||
do {
|
||||
double num = Math.sqrt(count);
|
||||
count++;
|
||||
tCurrent = System.currentTimeMillis();
|
||||
} while (tCurrent < tEnd);
|
||||
|
||||
System.out.println("[Result] " + count + " loops in " + dSeconds + "s");
|
||||
}
|
||||
}
|
||||
```
|
371
docs/content/common-usage-git-commands.md
Normal file
@@ -0,0 +1,371 @@
|
||||
---
|
||||
title: 常用的 Git 命令
|
||||
date: 2025-02-11T20:43:29Z
|
||||
lastmod: 2025-02-20T22:59:20Z
|
||||
tags: [Git,命令,开发]
|
||||
---
|
||||
|
||||
# 常用的 Git 命令
|
||||
|
||||
参考资料:[Git 大全 - Gitee.com](https://gitee.com/all-about-git)
|
||||
|
||||
## git config
|
||||
|
||||
### 编辑全局 Git 配置
|
||||
|
||||
```bash
|
||||
git config --global -e
|
||||
```
|
||||
|
||||
### 编辑当前 Git 配置
|
||||
|
||||
```bash
|
||||
# 默认为当前,“--local”可以省略
|
||||
git config --local -e
|
||||
```
|
||||
|
||||
### 配置用户名和邮箱
|
||||
|
||||
```text
|
||||
git config --global user.name '用户名'
|
||||
git config --global user.email '邮箱'
|
||||
```
|
||||
|
||||
### 开启 GPG 签名 commit
|
||||
|
||||
```text
|
||||
git config --global commit.gpgsign true
|
||||
```
|
||||
|
||||
注意:需要进行相关配置后才能正确开启([如何在 Gitee 上使用 GPG](https://gitee.com/help/articles/4248)、[使用 GPG 签名你的 commit](https://www.cnblogs.com/xueweihan/p/5430451.html))。
|
||||
|
||||
## git log
|
||||
|
||||
### 单行显示
|
||||
|
||||
`git log --oneline`
|
||||
|
||||
把每一条提交压缩到只有一行,仅保留短哈希、提价说明等最必要的信息,以一种更干净的方式查看提交。
|
||||
|
||||
### 显示差异
|
||||
|
||||
`git log -p`
|
||||
|
||||
展示带有改动内容的历史,可以看到每条提交都改动了哪些内容。
|
||||
|
||||
### 按作者过滤
|
||||
|
||||
添加参数 `--author` 以按作者过滤:
|
||||
|
||||
```text
|
||||
git log --author='example'
|
||||
```
|
||||
|
||||
Git 会使用正则来进行筛选和过滤,因此非准确的名字或大小写不一致也可以。
|
||||
|
||||
### 按时间过滤
|
||||
|
||||
添加参数 `--after` 和 `--before` 以按时间过滤。
|
||||
|
||||
2021-01-01 之后:
|
||||
|
||||
```text
|
||||
git log --after='2021-01-01'
|
||||
```
|
||||
|
||||
2022-01-01 到 2022-12-31 之间:
|
||||
|
||||
```text
|
||||
git log --after='2022-01-01' --before='2022-12-31'
|
||||
```
|
||||
|
||||
还可以使用以下格式:
|
||||
|
||||
```bash
|
||||
# 仅今天
|
||||
git log --after='today'
|
||||
|
||||
# 昨天以来
|
||||
git log --after='yesterday'
|
||||
|
||||
# 一周前以来
|
||||
git log --after='1 week ago'
|
||||
|
||||
# 十天之前
|
||||
git log --before='10 day ago'
|
||||
```
|
||||
|
||||
### 按提交信息过滤
|
||||
|
||||
添加参数 `--grep` 以使用正则表达式按提交信息过滤。
|
||||
|
||||
列出以“feat: ”开头的提交:
|
||||
|
||||
```text
|
||||
git log --grep='^feat: ' --oneline
|
||||
```
|
||||
|
||||
默认区分大小写,添加 `-i` 参数以不区分大小写:
|
||||
|
||||
```text
|
||||
git log -i --grep='^feat: ' --oneline
|
||||
```
|
||||
|
||||
多个条件:
|
||||
|
||||
```text
|
||||
git log --oneline --grep='^feat: \|^refactor: '
|
||||
```
|
||||
|
||||
### 列出某个文件的历史记录
|
||||
|
||||
单个文件:
|
||||
|
||||
```text
|
||||
git log index.html
|
||||
```
|
||||
|
||||
多个文件:
|
||||
|
||||
```text
|
||||
git log index.html index.js
|
||||
```
|
||||
|
||||
### 其它
|
||||
|
||||
```bash
|
||||
# 仅列出合并
|
||||
git log --merges
|
||||
|
||||
# 列出两个分支间的差异
|
||||
git log main..develop
|
||||
```
|
||||
|
||||
## git remote
|
||||
|
||||
刷新远程分支信息:
|
||||
|
||||
```text
|
||||
git remote update origin --prune
|
||||
```
|
||||
|
||||
## git reset
|
||||
|
||||
回退记录,保留文件:
|
||||
|
||||
```text
|
||||
git reset --soft head^
|
||||
```
|
||||
|
||||
## 其它命令
|
||||
|
||||
### 初始化 Git
|
||||
|
||||
在当前文件夹:
|
||||
|
||||
```text
|
||||
git init
|
||||
```
|
||||
|
||||
新建文件夹:
|
||||
|
||||
```text
|
||||
git init [directory]
|
||||
```
|
||||
|
||||
### 下载一个项目到本地
|
||||
|
||||
包含全部代码提交记录:
|
||||
|
||||
```text
|
||||
git clone [url]
|
||||
```
|
||||
|
||||
克隆指定分支:
|
||||
|
||||
```text
|
||||
git clone -b [branch] [url]
|
||||
git clone --branch [branch] [url]
|
||||
```
|
||||
|
||||
只克隆最近一次 commit:
|
||||
|
||||
```text
|
||||
git clone --depth=1 [url]
|
||||
```
|
||||
|
||||
### 分支
|
||||
|
||||
```bash
|
||||
# 列出所有本地分支
|
||||
git branch
|
||||
|
||||
# 列出所有远程分支
|
||||
git branch -r
|
||||
|
||||
# 列出所有本地分支和远程分支
|
||||
git branch -a
|
||||
|
||||
# 新建一个分支,但依然停留在当前分支
|
||||
git branch [branch-name]
|
||||
|
||||
# 新建一个分支,并切换到该分支
|
||||
git checkout -b [branch]
|
||||
|
||||
# 新建一个分支,指向指定commit
|
||||
git branch [branch] [commit]
|
||||
|
||||
# 新建一个分支,与指定的远程分支建立追踪关系
|
||||
git branch --track [branch] [remote-branch]
|
||||
|
||||
# 切换到指定分支,并更新工作区
|
||||
git checkout [branch-name]
|
||||
|
||||
# 切换到上一个分支
|
||||
git checkout -
|
||||
|
||||
# 建立追踪关系,在现有分支与指定的远程分支之间
|
||||
git branch --set-upstream [branch] [remote-branch]
|
||||
|
||||
# 合并指定分支到当前分支
|
||||
git merge [branch]
|
||||
|
||||
# 选择一个commit,合并进当前分支
|
||||
git cherry-pick [commit]
|
||||
|
||||
# 删除分支
|
||||
git branch -d [branch-name]
|
||||
|
||||
# 删除远程分支
|
||||
git push origin --delete [branch-name]
|
||||
git branch -dr [remote/branch]
|
||||
```
|
||||
|
||||
### 增加 / 删除文件
|
||||
|
||||
```bash
|
||||
# 添加指定文件到暂存区
|
||||
git add [file1] [file2] ...
|
||||
|
||||
# 添加指定目录到暂存区,包括子目录
|
||||
git add [dir]
|
||||
|
||||
# 添加当前目录的所有文件到暂存区
|
||||
git add .
|
||||
|
||||
# 添加每个变化前,都会要求确认
|
||||
# 对于同一个文件的多处变化,可以实现分次提交
|
||||
git add -p
|
||||
|
||||
# 删除工作区文件,并且将这次删除放入暂存区
|
||||
git rm [file1] [file2] ...
|
||||
|
||||
# 停止追踪指定文件,但该文件会保留在工作区
|
||||
git rm --cached [file]
|
||||
|
||||
# 改名文件,并且将这个改名放入暂存区
|
||||
git mv [file-original] [file-renamed]
|
||||
```
|
||||
|
||||
### 代码提交
|
||||
|
||||
```bash
|
||||
# 提交暂存区到仓库区
|
||||
git commit -m [message]
|
||||
|
||||
# 提交暂存区的指定文件到仓库区
|
||||
git commit [file1] [file2] ... -m [message]
|
||||
|
||||
# 提交工作区自上次 commit 之后的变化,直接到仓库区
|
||||
git commit -a
|
||||
|
||||
# 提交时显示所有diff信息
|
||||
git commit -v
|
||||
|
||||
# 使用一次新的 commit,替代上一次提交
|
||||
# 如果代码没有任何新变化,则用来改写上一次 commit 的提交信息
|
||||
git commit --amend -m [message]
|
||||
|
||||
# 重做上一次 commit,并包括指定文件的新变化
|
||||
git commit --amend [file1] [file2] ...
|
||||
```
|
||||
|
||||
### GC
|
||||
|
||||
自动判断:
|
||||
|
||||
```text
|
||||
git gc --auto
|
||||
```
|
||||
|
||||
更积极地优化存储库:
|
||||
|
||||
```text
|
||||
git gc --aggressive --prune=now
|
||||
```
|
||||
|
||||
### 修改历史提交内容
|
||||
|
||||
1. 查看提交记录:
|
||||
|
||||
```text
|
||||
git log --oneline
|
||||
```
|
||||
|
||||
假设需要修改的提交记录为 `e2394c2`。
|
||||
2. 通过 rebase 将 HEAD 回退到需要修改的位置前:
|
||||
|
||||
```text
|
||||
git rebase e2394c2^ --interactive
|
||||
```
|
||||
3. 在打开的编辑界面中将需要修改的提交前的 `pick` 改为 `edit`,然后保存退出。
|
||||
4. 修改文件,然后重新提交。
|
||||
|
||||
```text
|
||||
git add example.html
|
||||
git commit --amend
|
||||
```
|
||||
|
||||
注意:提交使用的参数是 `--amend`。
|
||||
5. 执行 `git rebase --continue` 命令逐步前进到最新的提交位置。
|
||||
注意:修改文件后可能会产生冲突,解决冲突并提交后需要再次执行 `git rebase --continue` 命令以继续。
|
||||
6. 提交到远程:
|
||||
|
||||
```text
|
||||
git push origin -f
|
||||
```
|
||||
|
||||
### 重置分支内容为另一分支
|
||||
|
||||
```text
|
||||
git checkout 操作分支名
|
||||
git reset --hard 另一分支名
|
||||
git push --force origin 操作分支名
|
||||
```
|
||||
|
||||
### 快速迁移仓库
|
||||
|
||||
#### 方式一,直接在原仓库的文件夹中操作,需要本地有全部分支和标签信息
|
||||
|
||||
```text
|
||||
git push --all 新的仓库地址
|
||||
git push --tags 新的仓库地址
|
||||
git remote set-url origin 新的仓库地址
|
||||
```
|
||||
|
||||
#### 方式二,在新的文件夹中操作,将原仓库的全部分支和标签拉取到本地然后推送到新仓库
|
||||
|
||||
```text
|
||||
git clone --bare 原仓库地址
|
||||
cd 仓库名称.git
|
||||
git push --all 新的仓库地址
|
||||
git push --tags 新的仓库地址
|
||||
```
|
||||
|
||||
回到原仓库的文件夹,更新远程地址
|
||||
|
||||
```text
|
||||
git remote set-url origin 新的仓库地址
|
||||
```
|
||||
|
||||
|
25
docs/content/ffmpeg-merge-video-files.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: 使用 FFmpeg 合并多个视频文件
|
||||
date: 2025-04-18T22:50:14Z
|
||||
lastmod: 2025-07-03T21:58:34Z
|
||||
tags: [FFmpeg,命令]
|
||||
---
|
||||
|
||||
# 使用 FFmpeg 合并多个视频文件
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. 将合并的视频文件都放在同一个文件夹下,例如:`file_1.flv` `file_2.flv`
|
||||
2. 新建一个 `video.txt` 文件,写入需要合并的视频文件名称,例如:
|
||||
|
||||
```plaintext
|
||||
file 'file_1.flv'
|
||||
file 'file_2.flv'
|
||||
```
|
||||
3. 执行 `ffmpeg` 命令:
|
||||
|
||||
```plaintext
|
||||
ffmpeg -f concat -i video.txt -c copy result.flv
|
||||
```
|
||||
|
||||
|
22
docs/content/fix-ant-tree-select-can-select-disabled-item.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: 解决 Ant TreeSelect(树选择)组件可以使用键盘选中 disabled(已禁用)项的问题
|
||||
date: 2025-02-11T20:55:27Z
|
||||
lastmod: 2025-02-20T22:56:23Z
|
||||
tags: [Web 前端,Vue.js]
|
||||
---
|
||||
|
||||
# 解决 Ant TreeSelect(树选择)组件可以使用键盘选中 disabled(已禁用)项的问题
|
||||
|
||||
最近在使用 Ant Design Vue(V3.2.20)的 TreeSelect 组件时发现一个问题:`tree-data` 中部分数据的 `disabled` 属性设置为了 `true`,选项是“禁用”状态,无法通过鼠标点击选中,但是可以通过键盘 `↑` `↓` 键切换选项,按下 `Enter` 键选中。
|
||||
|
||||

|
||||
|
||||
一开始还以为是 bug,后来通过查阅 [文档](https://3x.antdv.com/components/tree-select-cn#API) 和测试发现,该组件还有一个名为 `selectable` 的属性,用于控制选项是否可选。
|
||||
|
||||

|
||||
|
||||
仅将选项的 `selectable` 属性设置为 `false` 时,对应的选项虽然文本颜色不变,但是不可通过点击或键盘选中。
|
||||
|
||||

|
||||
|
||||
因此,如果要实现选项变为灰色且不可选的效果,需要同时将选项的 `disabled` 属性设置为 `true`,将 `seletable` 属性设置为 `false`。
|
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: 解决钉钉通过短链接跳转小程序 scheme 的方式打开小程序时会有一个空白页面的问题
|
||||
date: 2025-03-22T22:17:11Z
|
||||
lastmod: 2025-03-22T22:32:06Z
|
||||
tags: [钉钉,小程序,Web 前端]
|
||||
---
|
||||
|
||||
# 解决钉钉通过短链接跳转小程序 scheme 的方式打开小程序时会有一个空白页面的问题
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [小程序 scheme - 钉钉开放平台](https://open.dingtalk.com/document/orgapp/scheme-of-mini-programs)
|
||||
- [关闭当前页面 - 钉钉开放平台](https://open.dingtalk.com/document/orgapp/close-the-current-page)
|
||||
|
||||
- [钉钉 JS-API | 钉钉宜搭·开发者中心](https://dingtalk-yida.github.io/developer-site/docs/api/dingAPI/)
|
||||
|
||||
## 页面代码
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>小程序</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="result"></div>
|
||||
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.5/dingtalk.open.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', function () {
|
||||
let element = document.getElementById('test');
|
||||
let url = 'dingtalk://dingtalkclient/action/open_micro_app?corpId=<corpId>&miniAppId=<miniAppId>&agentId=<agentId>&pVersion=1&packageType=1&page=pages/HomeView/Index';
|
||||
// 打开小程序
|
||||
location.href = url;
|
||||
// 关闭当前页面
|
||||
window.dd.biz.navigation.close({
|
||||
onFail : function(error) {
|
||||
element.textContent = `操作失败:${error}`;
|
||||
},
|
||||
onSuccess: function() {
|
||||
element.textContent = `操作成功`;
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
59
docs/content/javascript-define-and-init-array.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: JavaScript 创建并初始化任意长度的数组
|
||||
date: 2025-02-11T20:45:30Z
|
||||
lastmod: 2025-02-20T22:59:05Z
|
||||
tags: [JavaScript,Web 前端]
|
||||
---
|
||||
|
||||
# JavaScript 创建并初始化任意长度的数组
|
||||
|
||||
## 直接定义
|
||||
|
||||
```javascript
|
||||
var arr = [0, 0, 0, 0, 0]; // [0, 0, 0, 0, 0]
|
||||
```
|
||||
|
||||
## 使用 push() 方法
|
||||
|
||||
```javascript
|
||||
var arr = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
arr.push(0);
|
||||
}
|
||||
|
||||
// [0, 0, 0, 0, 0]
|
||||
```
|
||||
|
||||
## 使用 Array 构造函数和 fill() 方法
|
||||
|
||||
```javascript
|
||||
var arr = new Array(5); // [empty × 5]
|
||||
arr.fill(0); // [0, 0, 0, 0, 0]
|
||||
```
|
||||
|
||||
## 使用 Array 构造函数和数组展开
|
||||
|
||||
```javascript
|
||||
var arr = [...new Array(5)]; // [undefined x 5]
|
||||
```
|
||||
|
||||
```javascript
|
||||
var arr = [...new Array(5).keys()]; // [0, 1, 2, 3, 4]
|
||||
```
|
||||
|
||||
## 使用 Array.from()
|
||||
|
||||
> `Array.from(arrayLike[, mapFn[, thisArg]])`
|
||||
|
||||
```javascript
|
||||
var arr = Array.from({length: 5}); // [undefined x 5]
|
||||
```
|
||||
|
||||
```javascript
|
||||
var arr = Array.from({length: 5}, () => 0); // [0, 0, 0, 0, 0]
|
||||
```
|
||||
|
||||
```javascript
|
||||
var arr = Array.from({length: 5}, (v, i) => (i + 1)); // [1, 2, 3, 4, 5]
|
||||
```
|
31
docs/content/miui-dial-plate-commands.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: MIUI 拨号盘指令(代码)合集
|
||||
date: 2025-02-11T20:55:13Z
|
||||
lastmod: 2025-02-20T22:58:12Z
|
||||
tags: [Android,小米,系统,HyperOS,MIUI]
|
||||
---
|
||||
|
||||
# MIUI 拨号盘指令(代码)合集
|
||||
|
||||
参考:[www.coolapk.com](https://www.coolapk.com/feed/50745332?shareKey=YWVjMDU3MjRjNGFhNjU0NTg5MGM~&shareUid=1033889)
|
||||
|
||||
部分指令也支持在澎湃 OS 上使用
|
||||
|
||||
```text
|
||||
*#*#54638#*#* 显示 5G 开关
|
||||
*#*#8667#*#* 显示 VoNR 开关
|
||||
*#*#86583#*#* 显示 VoLTE 选项
|
||||
*#*#869434#*#* 显示 VoWiFi 选项
|
||||
*#*#726633#*#* 开启 5G 网络模式选择
|
||||
*#*#4636#*#* 手机信息锁网(查看手机信号强弱)
|
||||
*#*#6484#*#* cit 工程模式
|
||||
*#*#64663#*#* 手机功能测试
|
||||
*#*#3223#*#* 开启 DC 调光选项
|
||||
*#*#284#*#* 生成 bug 报告(可查看手机电池剩余容量)
|
||||
*#*#6485#*#* 充电与电池相关的信息
|
||||
*#*#76937#*#* 神隐模式
|
||||
*#*#37263#*#* 帧率显示
|
||||
*#*#3646633#*#* MTK 工程模式(可用于锁频段)
|
||||
*#*#6666#*#* 退出演示模式(仅展示机可用)
|
||||
*#06# 移动识别码 IMEI号
|
||||
```
|
70
docs/content/nodejs-check-is-tablet-pc.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Electron 或 Node.js 判断当前设备是否支持触摸屏
|
||||
date: 2025-02-11T20:55:18Z
|
||||
lastmod: 2025-02-20T22:57:46Z
|
||||
tags: [JavaScript,Node.js,Electron,Web 前端]
|
||||
---
|
||||
|
||||
# Electron 或 Node.js 判断当前设备是否支持触摸屏
|
||||
|
||||
在 Windows 系统上,可以通过注册表项 `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Tablet PC\IsTabletPC` 获取当前设备是否支持触摸屏。
|
||||
|
||||
若 `IsTabletPC` 的值大于 `0`,则表示支持触摸屏。
|
||||
|
||||

|
||||
|
||||
执行以下命令,可以查询到对应的注册表信息:
|
||||
|
||||
```text
|
||||
REG QUERY "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Tablet PC" /v IsTabletPC
|
||||
```
|
||||
|
||||

|
||||
|
||||
在 Node.js 中,可以通过执行上述命令,检测返回内容的方式,判断当前设备是否支持触摸屏(除了使用命令以外,还可以使用 [regedit](https://www.npmjs.com/package/regedit) 模块来获取注册表信息)。
|
||||
|
||||
示例代码:
|
||||
|
||||
```javascript
|
||||
const cp = require('child_process');
|
||||
const os = require('os');
|
||||
|
||||
/** 通过注册表信息检测当前设备是否支持触摸屏 */
|
||||
function isTabletPC() {
|
||||
|
||||
if (os.platform() !== 'win32') {
|
||||
throw new Error('仅支持 Windows 系统');
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// 命令返回内容:
|
||||
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Tablet PC
|
||||
// IsTabletPC REG_DWORD 0x0
|
||||
|
||||
// 命令内容
|
||||
let cmd = 'REG QUERY "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\Tablet PC" /v IsTabletPC';
|
||||
|
||||
// 执行命令,转换返回内容为字符串
|
||||
let result = cp.execSync(cmd).toString();
|
||||
|
||||
// 匹配:IsTabletPC REG_DWORD 0x0
|
||||
let matched = result.match(/IsTabletPC\s+REG_\w+\s+([0-9a-fx]+)/i);
|
||||
|
||||
// 提取:0x0
|
||||
let value = parseInt(matched ? matched[1] : null);
|
||||
|
||||
if (isNaN(value)) {
|
||||
throw new Error('处理命令返回内容失败');
|
||||
}
|
||||
|
||||
return (value !== 0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('检测触摸屏失败:');
|
||||
console.error(String(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
32
docs/content/post-2025-02-11-1.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 在用 uni-app 开发钉钉小程序的时候遇到一个奇怪的问题,发送请求拿不到返回的数据
|
||||
date: 2025-02-11T20:55:40Z
|
||||
lastmod: 2025-02-12T09:51:42Z
|
||||
tags: [Web 前端,uni-app,小程序]
|
||||
---
|
||||
|
||||
# 在用 uni-app 开发钉钉小程序的时候遇到一个奇怪的问题,发送请求拿不到返回的数据
|
||||
|
||||
今天我一位同事说用 uni-app 新开发的钉钉小程序里发送请求拿不到返回的数据,看了下发现调试工具的“Network”栏里显示请求是发送成功的,也有返回数据,但是没触发请求的回调函数。
|
||||
|
||||
原本用的是 `luch-request` 这个库发送的请求,后来试了下 uni-app 内置的 `uni.request` 以及钉钉的 `dd.httpRequest` 都是一样不行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
再后来重新创建一个新的项目试了一下是可以的,就觉得是版本的问题,然而把依赖项的版本改成和新建的一样了也不行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
看来看去,最后发现原来是这个 `options.value = options` 导致的(刚开始试过在 `App.vue` 里写请求也不行就没怎么在意这里),把它去掉就正常了。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
然后试了下在新创建的项目里写上这个 `options.value = options`,但是这个问题并没有出现。
|
50
docs/content/use-css-function-in-less.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: 在 Less 中使用与 Less 内置函数同名的原生 CSS 函数
|
||||
date: 2025-02-11T20:47:17Z
|
||||
lastmod: 2025-02-20T22:58:47Z
|
||||
tags: [Web 前端,CSS,Less]
|
||||
---
|
||||
|
||||
# 在 Less 中使用与 Less 内置函数同名的原生 CSS 函数
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [calc() - CSS:层叠样式表 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/calc)
|
||||
- [Getting started - Escaping | Less.js](https://lesscss.org/#escaping)
|
||||
- [Using The CSS Function calc() Inside The LESS CSS Preprocessor](https://www.bennadel.com/blog/4047-using-the-css-function-calc-inside-the-less-css-preprocessor.htm)
|
||||
|
||||
## 问题描述
|
||||
|
||||
在原生 CSS 中有以下的函数:`calc()`、`max()`、`min()` 等,而在 Less 中也有同名的函数,使用的时候可能会冲突,无法得到需要的结果。
|
||||
|
||||
对于 `calc()`,Less 进行了处理,不会对数学表达式进行计算。
|
||||
|
||||

|
||||
|
||||
但如果其中包含变量或嵌套的函数,则会进行计算。例如 `calc()` 和 `max()` 嵌套使用的时候:
|
||||
|
||||
```css
|
||||
.element {
|
||||
width: calc(max(var(--min-width), var(--item-width) + var(--offset-width)) * 1px);
|
||||
}
|
||||
```
|
||||
|
||||
会出现以下报错:
|
||||
|
||||
```text
|
||||
[less] Error evaluating function `max`: Operation on an invalid type
|
||||
```
|
||||
|
||||
## 如何解决
|
||||
|
||||
这时可以使用 Less 的转义字符:在字符串前加上一个 `~` 符号,并将需要转义的字符串放在 `""` 或 `''` 中。
|
||||
|
||||

|
||||
|
||||
```css
|
||||
.element {
|
||||
width: ~"calc(max(var(--min-width), var(--item-width) + var(--offset-width)) * 1px)";
|
||||
}
|
||||
```
|
||||
|
||||
这样就可以使用任意的字符串作为属性或变量值了(当然,前提是使用正确的 CSS 语法)。
|
140
docs/content/windows-copy-command-usage.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Windows copy 命令的妙用(文件里藏文件、合并文件)
|
||||
date: 2025-03-22T22:47:31Z
|
||||
lastmod: 2025-03-29T18:30:31Z
|
||||
tags: [Windows,命令,命令提示符]
|
||||
---
|
||||
|
||||
# Windows copy 命令的妙用(文件里藏文件、合并文件)
|
||||
|
||||
## 说明和注意事项
|
||||
|
||||
- `copy` 是一个基础的DOS命令,也许在今天已经很少有人使用,但它仍然很实用。
|
||||
|
||||
- 本教程基于 Windows 10 x64 专业版。
|
||||
|
||||
## 本教程中的命令格式
|
||||
|
||||
- `copy /B 文件名1+文件名2 合并后的文件名`
|
||||
- `copy /B *.扩展名 合并后的文件名`
|
||||
|
||||
## 使用前准备
|
||||
|
||||
运行“命令提示符”的常用方法:同时按下 Win 键和 R 键,输入 `cmd`,按下 Enter(回车)键。
|
||||
|
||||

|
||||
|
||||
可能需要“以管理员身份运行”命令提示符,其中一种方法:
|
||||
|
||||
1. 右键任务栏
|
||||
2. 点击任务管理器
|
||||
3. 依次点击“文件”->“运行新任务”
|
||||
|
||||

|
||||
4. 输入 `cmd`,勾选“以系统管理权限创建此任务”,按下 `Enter`(回车)键
|
||||
|
||||

|
||||
|
||||
## 把文件藏进文件里
|
||||
|
||||
### 说明
|
||||
|
||||
根据测试,可支持把文件放入格式包括但不限于 GIF、JPG、PNG、MP3、OGG、FLV、MP4 的文件中
|
||||
|
||||
使用的压缩软件:2345 好压
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 把要藏的文件压缩(ZIP 格式)
|
||||
|
||||

|
||||
|
||||
2. 把压缩包和用于藏文件的文件放到同一文件夹
|
||||
|
||||
3. 使用 `cd /D` 命令 切换到第 2 步的文件夹
|
||||
|
||||
例如我第2步的文件夹在 `D:\Files\Desktop\教程-命令提示符-copy`
|
||||
|
||||
则执行命令 `cd /D D:\Files\Desktop\教程-命令提示符-copy`
|
||||
|
||||
看到左侧显示切换后的路径则更改成功
|
||||
|
||||

|
||||
|
||||
4. 使用 `copy /B` 命令 合并文件
|
||||
|
||||
命令格式:`copy /B 压缩包名+用于藏文件的文件的文件名 合并后的文件名`
|
||||
|
||||
文件的顺序不能错,否则合并后的文件无法正常打开
|
||||
|
||||
例如我的压缩包名为 `Files.zip`,用于藏文件的文件的文件名为 `Picture.png`
|
||||
|
||||
则执行命令 `copy /B Picture.png+Files.zip Picture-Merge.png`
|
||||
|
||||

|
||||
|
||||
5. 若文件合并正常(达到想要的效果)则可以删除压缩包和原图片(Files.zip、Picture.png)
|
||||
|
||||
注意:
|
||||
|
||||
用于藏文件的文件的文件名与合并后的文件名的扩展名需要相同,但前缀名不能相同
|
||||
|
||||
例如 `Picture.png` 与 `Picture-Merge.png`
|
||||
|
||||
## 提取隐藏的文件
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 右键合并后的文件,点击重命名(可以直接选中合并后的文件,按下 F2)
|
||||
|
||||

|
||||
|
||||
2. 在文件名后方加上 `.zip`
|
||||
|
||||
例如 `Picture-Merge.png` 改为 `Picture-Merge.png.zip`
|
||||
|
||||

|
||||
|
||||
然后双击文件,以压缩包方式打开即可
|
||||
|
||||

|
||||
|
||||
## 合并分段视频
|
||||
|
||||
### 说明
|
||||
|
||||
根据测试,可支持合并格式包括但不限于 MP4、TS、M2TS 的分段视频文件。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 把视频文件放于同一文件夹内
|
||||
|
||||
2. 视频文件的文件名需为 `序号.扩展名`
|
||||
|
||||
例如 `001.mp4 002.mp4 003.mp4 004.mp4 ......`
|
||||
|
||||
序号不能错,否则合并的视频内容会出错。
|
||||
|
||||
3. 使用 `cd /D` 命令 切换到要第 1 步的文件夹
|
||||
|
||||
例如我第 1 步的文件夹在 `D:\Files\Desktop\教程-命令提示符-copy`
|
||||
|
||||
则执行命令 `cd /D D:\Files\Desktop\教程-命令提示符-copy`
|
||||
|
||||
看到左侧显示切换后的路径则更改成功
|
||||
|
||||

|
||||
|
||||
4. 使用 `copy /B 命令` 合并文件
|
||||
|
||||
命令格式:`copy /B *.扩展名 合并后的文件名`
|
||||
|
||||
例如我的分段视频文件名为 `Test_001.mp4 Test_002.mp4 Test_003.mp4 Test_004.mp4 ......`
|
||||
|
||||
则执行命令 `copy /B Test_*.mp4 Merge.mp4`
|
||||
|
||||

|
||||
|
||||
5. 测试合并后的视频文件能否正常播放,若能正常播放,则合并成功
|
||||
|
||||
|
414
docs/content/zlm-rtc-client-multi-video-pull-once.md
Normal file
@@ -0,0 +1,414 @@
|
||||
---
|
||||
title: 通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次
|
||||
date: 2025-02-11T20:55:33Z
|
||||
lastmod: 2025-02-12T09:50:12Z
|
||||
tags: [Web 前端,JavaScript,视频流]
|
||||
---
|
||||
|
||||
# 通过画布(Canvas)实现 ZLMRTCClient 同一视频流多次显示时只拉取一次
|
||||
|
||||
## 效果预览
|
||||
|
||||
视频画面
|
||||
|
||||

|
||||
|
||||
网络请求
|
||||
|
||||

|
||||
|
||||
## 代码实现
|
||||
|
||||
### ZLMRTCClient.js
|
||||
|
||||
> 当前使用的版本:
|
||||
> `1.0.1` `Mon Mar 27 2023 19:11:59 GMT+0800`
|
||||
|
||||
首先需要修改 ZLMRTCClient.js 的代码,解决由于网络导致播放失败时无法触发 `WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED` 事件的问题。
|
||||
|
||||
修改前:
|
||||
|
||||

|
||||
|
||||
修改后:
|
||||
|
||||

|
||||
|
||||
修改内容:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```javascript
|
||||
// 添加 catch()
|
||||
axios({
|
||||
}).then(() => {
|
||||
}).catch(() => {
|
||||
// 网络异常时触发事件
|
||||
this.dispatch(Events$1.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, null);
|
||||
});
|
||||
```
|
||||
|
||||
### video-preview.js
|
||||
|
||||
```javascript
|
||||
// 2024-05-30 初始版本
|
||||
// 2024-06-06 优化视频是否存在调用检测方式
|
||||
// 2025-01-08 优化逻辑,减少定时器的使用
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* @typedef CacheItem
|
||||
* @property {string} id 缓存项唯一 ID
|
||||
* @property {HTMLElement|null} element Video 元素
|
||||
* @property {boolean} isStopped 是否为主动停止播放
|
||||
* @property {ZLMPlayer|null} player ZLM 播放器对象
|
||||
* @property {number} timeCheck 最后一次检测关联画布的时间戳
|
||||
* @property {number} timeResize 最后一次更新分辨率的时间戳
|
||||
* @property {number} timeRender 最后一次渲染的时间戳
|
||||
* @property {boolean} willStop 是否没有关联的画布,在下一次停止播放
|
||||
*/
|
||||
|
||||
/** @typedef {InstanceType<typeof ZLMRTCClient.Endpoint>} ZLMPlayer */
|
||||
|
||||
/** 检测视频是否存在调用间隔 */
|
||||
const INTERVAL_CHECK_VIDEO = 10000;
|
||||
|
||||
/** 画布渲染间隔 */
|
||||
const INTERVAL_RENDER = 100;
|
||||
|
||||
/** 画布分辨率更新间隔 */
|
||||
const INTERVAL_RESIZE = 1000;
|
||||
|
||||
/** 循环处理间隔 */
|
||||
const INTERVAL_TIME = 100;
|
||||
|
||||
/** 模块名称 */
|
||||
const PREFIX = '[video-preview]';
|
||||
|
||||
/** 重新播放间隔 */
|
||||
const RESTART_TIMEOUT = 2000;
|
||||
|
||||
/** ZLM 客户端 */
|
||||
const ZLMRTCClient = window.ZLMRTCClient;
|
||||
|
||||
/** 循环检测定时器 */
|
||||
let loopTimer = null;
|
||||
|
||||
/**
|
||||
* @desc 缓存信息列表
|
||||
* @type {Record<string, CacheItem | null>}
|
||||
*/
|
||||
export const cacheList = {};
|
||||
|
||||
/**
|
||||
* @description 初始化播放器
|
||||
* @param {string} url 视频流地址
|
||||
*/
|
||||
function initPlayer(url = '') {
|
||||
try {
|
||||
|
||||
if (!url) {
|
||||
throw new Error('缺少 url 参数');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化 & 更新数据
|
||||
* @param {CacheItem} cache
|
||||
*/
|
||||
let fnInit = (cache) => {
|
||||
|
||||
// 创建 video 元素
|
||||
let element = document.createElement('video');
|
||||
|
||||
// 开启自动播放
|
||||
// 注:不能用 `setAttribute`,否则没效果
|
||||
element.autoplay = true;
|
||||
element.controls = false;
|
||||
element.muted = true;
|
||||
|
||||
// 标记缓存 ID
|
||||
element.setAttribute('data-video-id', cache.id);
|
||||
|
||||
// 添加到页面,否则无法播放
|
||||
element.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0');
|
||||
document.body.appendChild(element);
|
||||
|
||||
// 初始化播放器
|
||||
let player = new ZLMRTCClient.Endpoint({
|
||||
// video 标签
|
||||
element: element,
|
||||
// 是否打印日志
|
||||
debug: false,
|
||||
// 流地址
|
||||
zlmsdpUrl: url,
|
||||
// 功能开关
|
||||
audioEnable: false,
|
||||
simulcast: false,
|
||||
useCamera: false,
|
||||
videoEnable: true,
|
||||
// 仅查看,不推流
|
||||
recvOnly: true,
|
||||
// 推流分辨率
|
||||
resolution: { w: 1280, h: 720 },
|
||||
// 文本收发
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/send
|
||||
usedatachannel: false,
|
||||
});
|
||||
|
||||
// // 监听事件:ICE 协商出错
|
||||
// player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, function () {
|
||||
// console.error(PREFIX, 'ICE 协商出错')
|
||||
// });
|
||||
|
||||
// 监听事件:获取到了远端流,可以播放
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, function (event) {
|
||||
console.log(PREFIX, '播放成功', event.streams);
|
||||
});
|
||||
|
||||
// 监听事件:offer anwser 交换失败
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, function (event) {
|
||||
|
||||
console.error(PREFIX, 'offer anwser 交换失败', event);
|
||||
|
||||
// 当前没有主动停止
|
||||
if (!cache.isStopped) {
|
||||
// 停止播放
|
||||
stopPlayer(player, element);
|
||||
// 重新播放
|
||||
setTimeout(() => {
|
||||
fnInit(cache);
|
||||
}, RESTART_TIMEOUT);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 监听事件:RTC 状态变化
|
||||
player.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, function (state) {
|
||||
|
||||
console.log(PREFIX, 'RTC 状态变化', state);
|
||||
|
||||
// 状态为已断开
|
||||
if (state === 'disconnected' && !cache.isStopped) {
|
||||
// 停止播放
|
||||
stopPlayer(player, element);
|
||||
// 重新播放
|
||||
setTimeout(() => {
|
||||
fnInit(cache);
|
||||
}, RESTART_TIMEOUT);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
cache.element = element;
|
||||
cache.player = player;
|
||||
|
||||
};
|
||||
|
||||
let cacheItem = cacheList[url];
|
||||
|
||||
if (cacheItem) {
|
||||
return cacheItem;
|
||||
} else {
|
||||
cacheItem = {
|
||||
id: uuidv4(),
|
||||
element: null,
|
||||
isStopped: false,
|
||||
player: null,
|
||||
timeCheck: 0,
|
||||
timeRender: 0,
|
||||
timeResize: 0,
|
||||
willStop: false,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(PREFIX, '初始化', cacheItem);
|
||||
|
||||
// 初始化
|
||||
fnInit(cacheItem);
|
||||
|
||||
// 添加缓存信息
|
||||
cacheList[url] = cacheItem;
|
||||
|
||||
return cacheItem;
|
||||
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '初始化播放器失败:');
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 停止播放
|
||||
* @param {ZLMPlayer} player
|
||||
* @param {HTMLVideoElement} element
|
||||
*/
|
||||
function stopPlayer(player, element) {
|
||||
try {
|
||||
|
||||
if (player) {
|
||||
console.debug(PREFIX, 'stopPlayer - 停止播放');
|
||||
player.close();
|
||||
}
|
||||
|
||||
if (element instanceof HTMLVideoElement) {
|
||||
console.debug(PREFIX, 'stopPlayer - 移除元素');
|
||||
element.remove();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '停止播放失败:');
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取视频画面 canvas
|
||||
* @param {string} url
|
||||
*/
|
||||
export function getVideoCanvas(url = '') {
|
||||
try {
|
||||
|
||||
if (!url) {
|
||||
throw new Error('缺少 url 参数');
|
||||
}
|
||||
|
||||
let cacheItem = initPlayer(url);
|
||||
let canvas = document.createElement('canvas');
|
||||
|
||||
if (cacheItem) {
|
||||
// 标记缓存 ID
|
||||
canvas.setAttribute('data-cache-id', cacheItem.id);
|
||||
} else {
|
||||
throw new Error('获取缓存数据失败');
|
||||
}
|
||||
|
||||
// 背景填充
|
||||
canvas.style.backgroundPosition = 'center center';
|
||||
canvas.style.backgroundSize = '100% 100%';
|
||||
|
||||
return canvas;
|
||||
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '获取 canvas 失败:');
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 开始循环处理视频 */
|
||||
export function timerStart() {
|
||||
timerStop();
|
||||
loopTimer = setInterval(() => {
|
||||
|
||||
for (let url in cacheList) {
|
||||
|
||||
let cacheItem = cacheList[url];
|
||||
let currTime = Date.now();
|
||||
|
||||
if (!cacheItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cacheId = cacheItem.id;
|
||||
let videoElement = cacheItem.element;
|
||||
|
||||
/**
|
||||
* @desc 画布元素列表
|
||||
* @type {NodeListOf<HTMLCanvasElement>}
|
||||
*/
|
||||
let canvasList = document.querySelectorAll(`[data-cache-id="${cacheId}"]`);
|
||||
let foundCanvas = canvasList.length > 0;
|
||||
|
||||
// 渲染画面
|
||||
if (currTime - cacheItem.timeRender >= INTERVAL_RENDER) {
|
||||
cacheItem.timeRender = currTime;
|
||||
canvasList.forEach((canvas) => {
|
||||
|
||||
let ctx = canvas.getContext('2d');
|
||||
let cWidth = canvas.width;
|
||||
let cHeight = canvas.height;
|
||||
|
||||
if (document.contains(videoElement)) {
|
||||
ctx.drawImage(videoElement, 0, 0, cWidth, cHeight);
|
||||
}
|
||||
|
||||
canvas.style.backgroundImage = '';
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// 更新画布分辨率
|
||||
if (currTime - cacheItem.timeResize >= INTERVAL_RESIZE) {
|
||||
cacheItem.timeResize = currTime;
|
||||
canvasList.forEach((canvas) => {
|
||||
|
||||
let parent = canvas.parentElement;
|
||||
let rect = parent ? parent.getBoundingClientRect() : null;
|
||||
|
||||
if (rect) {
|
||||
|
||||
let cWidth = Math.round(canvas.width);
|
||||
let cHeight = Math.round(canvas.height);
|
||||
|
||||
let rWidth = Math.round(rect.width);
|
||||
let rHeight = Math.round(rect.height);
|
||||
|
||||
if (cWidth !== rWidth || cHeight !== rHeight) {
|
||||
// 更新画布分辨率前将画面设置为背景,防止闪烁
|
||||
canvas.style.backgroundImage = `url(${canvas.toDataURL('image/png')})`;
|
||||
// 更新画布分辨率(将会自动清空画布内容)
|
||||
canvas.width = rWidth;
|
||||
canvas.height = rHeight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// 检测是否存在与视频关联的画布
|
||||
if (currTime - cacheItem.timeCheck >= INTERVAL_CHECK_VIDEO) {
|
||||
|
||||
cacheItem.timeCheck = currTime;
|
||||
|
||||
// 当前存在关联的画布,不处理
|
||||
if (foundCanvas) {
|
||||
cacheItem.willStop = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 若当前不存在关联的画布,检测上一次的查找结果
|
||||
if (cacheItem.willStop) {
|
||||
console.debug(PREFIX, '视频没有被调用,停止播放', { url });
|
||||
cacheItem.isStopped = true;
|
||||
stopPlayer(cacheItem.player, cacheItem.element);
|
||||
cacheList[url] = null;
|
||||
} else {
|
||||
cacheItem.willStop = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, INTERVAL_TIME);
|
||||
}
|
||||
|
||||
/** 停止循环处理视频 */
|
||||
export function timerStop() {
|
||||
if (loopTimer) {
|
||||
clearInterval(loopTimer);
|
||||
loopTimer = null;
|
||||
}
|
||||
}
|
||||
JAVASCRIPT 折叠 复制 全屏
|
||||
```
|
||||
|
||||
使用时只需要调用 `getVideoCanvas()` 获取 `canvas`,然后插入到 DOM 即可,画布会自适应父元素宽高。
|