Docker形式安装MySQL8

记于:2024-09-22 下午
地点:浙江省·温州市·家里
天气:雨天

背景#

项目需要,以docker形式安装MySQL8

命令#

1
2
3
4
5
docker run --name=mysql8 \
-p 3306:3306 \
--mount type=bind,src=/root/ysm.d/data/docker/mysql/conf/my.cnf,dst=/etc/my.cnf \
--mount type=bind,src=/root/ysm.d/data/docker/mysql/datadir,dst=/var/lib/mysql \
--restart always -d container-registry.oracle.com/mysql/community-server:8.4

配置文件和数据目录需要提前创建好
mkdir -p /root/ysm.d/data/docker/mysql/datadir -p /root/ysm.d/data/docker/mysql/conf
/root/ysm.d/data/docker/mysql/conf/my.cnf 内容如下(拷贝自mysql8.4默认配置):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/8.4/en/server-configuration-defaults.html

[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M

host-cache-size=0
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
secure-file-priv=/var/lib/mysql-files
user=mysql

pid-file=/var/run/mysqld/mysqld.pid

启动后查看日志:

1
docker logs mysql8

获取初始密码,登录后进行修改

1
2
3
4
5
6
7
8
# 执行容器内mysql登录
docker exec -it mysql8 mysql -uroot -p
# 更新root密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password' ;
# 更新mysql.user表,将root用户的host字段改为%,最后刷新权限
use mysql;
update user set host = '%' where user = 'root' ;
flush privileges;

参考资料#

点阵图Java实现

记于:2024-07-14 晚上
地点:浙江省·温州市·家里
天气:多云

背景#

因项目需要,需要参考项目PHP版本中的点阵图生成功能,实现Java版本逻辑。

功能#

1.生成点阵图(画布)
2.绘制文本
3.绘制图片
4.绘制二维码、条形码
5.其他,略

实现思路#

主要是构建一个(二维数组)点阵模型,在其上根据坐标绘制各种素材(文本、图片、二维码、条形码);
在Java中可以使用java.atw.image.BufferedImage类来表示点阵模型(画布),使用Graphics2D类来绘制各种素材;
每种类型素材生成并返回一个BufferedImage对象,然后将其绘制到画布上。

绘制素材#

绘制二维码、条形码以及自定义图片都相对简单,最麻烦的是绘制文本,因为点阵形式下需要自行解析字体文件,并计算生成每个字符的点阵数据;
根据参考项目(LatticePHP)中的说明,是因为:普通的字体因为加了锐角、美化是不能直接用来生成点阵图的,必须使用点阵字体。但是这方面的市场非常小,所以做的人很少,仅有的几个还会收取高昂费用。

绘制图片素材#

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public BufferedImage generateImage(String url) {
log.info("generateImage...url: {}", url);

URL url0;
try {
url0 = new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
BufferedImage bufferedImage;
try {
bufferedImage = ImageIO.read(url0);
} catch (IOException e) {
throw new RuntimeException(e);
}
return bufferedImage;
}

直接从网络地址读取图片,生成BufferedImage对象。

绘制二维码、条形码#

用到了zxing库,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 生成二维码
*/
public BufferedImage generateQrCode(String text, int width, int height) {
log.info("generateQrCode...text: {}, width: {}, height: {}", text, width, height);

// 生成二维码BitMatrix
BitMatrix bitMatrix;
try {
// 不能按指定大小生成,先按自动大小生成,在convertToDotMatrixImage方法中缩放
bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, 0, 0, this.hints);
} catch (WriterException e) {
throw new RuntimeException(e);
}
// 转换为点阵图像
return this.convertToDotMatrixImage(bitMatrix, width, height);
}

/**
* 生成条形码
*/
public BufferedImage generateBarCode(String text, int width, int height) {
log.info("drawBarCode...text: {}, width: {}, height: {}", text, width, height);

// 生成条形码BitMatrix
BitMatrix bitMatrix = new Code128Writer().encode(text, BarcodeFormat.CODE_128, 0, 0);
// 转换为点阵图像
return this.convertToDotMatrixImage(bitMatrix, width, height);
}

/**
* 将BitMatrix转换为点阵图像BufferedImage
* 生成二维码场景,当指定90x90,会生成49x49(bitMatrix尺寸是90x90但是内容是49x49居中)的二维码,需要进行缩放
*/
private BufferedImage convertToDotMatrixImage(BitMatrix bitMatrix, int desiredWidth, int desiredHeight) {
// 缩放二维码
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
BufferedImage scaledImage = new BufferedImage(desiredWidth, desiredHeight, BufferedImage.TYPE_BYTE_BINARY);
Graphics2D g = scaledImage.createGraphics();
g.drawImage(qrImage, 0, 0, desiredWidth, desiredHeight, null);
g.dispose();

return scaledImage;
}

遇到的问题:生成二维码时,按指定大小(90x90)生成不能生效;
原因:略,见参考资料(二维码尺寸问题);
解决:先按自动大小生成,然后在convertToDotMatrixImage方法按指定大小缩放。

绘制文本素材#

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
    /**
* 生成文本图像
*/
private BufferedImage generateTextImage(String text) throws IOException {
// 获取字体文件对象
// RandomAccessFile fontFile = this.getFontFile(this.fontFileName);

int fontWidth = this.fontWidth;
int fontHeight = this.fontHeight;
int byteCount = this.byteCount;

int textLength = text.length();

// list of char[][] 存放每个字符的点阵数据
ArrayList<char[][]> dotMatList = new ArrayList<>(textLength);

// 绘制每个字符
for (int i = 0; i < textLength; i++) {
char word = text.charAt(i);

// 获取字符码点
int codePoint = this.getCodePoint(String.valueOf(word));
// 修正24宽度字体的码点 ; 原因未知,copy自php版逻辑
codePoint = this.fixCodePointWhenFontWidth24(codePoint);
// 计算在字体文件中的位置
int position = codePoint * byteCount;
// 获取字体数据
// byte[] fontData = this.getFontData(fontFile, position, byteCount);
// fix: resources下的文件在打成jar后使用(RandomAccess)File读取失败,改为直接加载byte[]缓存到内存
byte[] fontData = this.readFontData(this.fontFileName, position, byteCount);
// convert to mat
char[][] dotMat = this.convertBytesToDotMat(fontData, fontHeight, fontWidth);

// 获取字符实际宽度
int realWidth = codePoint < 700 ? this.fontEn : this.fontZh;
// 按实际宽度切割重组
for (int j = 0; j < fontHeight; j++) {
// 自动宽度逻辑
realWidth = this.autoWidth ? this.getCharWidth(dotMat, this.fontZh, this.fontWidth) : realWidth;
// 空格间距逻辑
realWidth = codePoint == this.SPACE_CODE_POINT ? this.SPACE_WIDTH : realWidth;
// 按实际宽度切割
char[] row = dotMat[j];
char[] newRow = new char[realWidth];
System.arraycopy(row, 0, newRow, 0, realWidth);
// 加粗逻辑
newRow = this.setBold(newRow, this.bold);
// 字间距逻辑
newRow = this.setSpacing(newRow, this.fontSpace);

dotMat[j] = newRow;
}
// dotMat根据最长的长度补齐尾部
this.padDotMat(dotMat);

// 打印
// this.printDotMat(dotMat);
// 加入集合
dotMatList.add(dotMat);
}

// 整合所有dotMat from dotMatList
char[][] finalDotMat = this.mergeDotMat(dotMatList);

// 保存文本点阵宽度和高度
this.textBitWidth = finalDotMat[0].length;
this.textBitHeight = finalDotMat.length;

// 将所有dotMat绘制到图像
BufferedImage textImage = new BufferedImage(finalDotMat[0].length, fontHeight, BufferedImage.TYPE_BYTE_BINARY);
this.renderDotMatToImage(finalDotMat, textImage, 0, 0);

// fontFile.close();

return textImage;
}

/**
* 获取码点
*/
private int getCodePoint(String str) {
int codePoint = 0;
try {
// 获取字符串的代码点
codePoint = str.codePointAt(0);
} catch (IndexOutOfBoundsException e) {
// 处理字符串为空或其他异常情况
e.printStackTrace();
}
// 字符越界处理
if (codePoint > 65535) {
codePoint = 0;
}
return codePoint;
}

/**
* 修正24宽度字体的码点
* 将超出范围的码点修正为空格码点
*/
private int fixCodePointWhenFontWidth24(int codePoint) {
if (this.fontWidth == 24) {
if (codePoint <= this.SPACE_CODE_POINT || codePoint > this.fontMaxPosition) {
codePoint = this.SPACE_CODE_POINT;
}
}
return codePoint;
}

/**
* 转换字节数组为点阵多维数组
*/
private char[][] convertBytesToDotMat(byte[] fontData, int fontHeight, int fontWidth) {
// fontData bit -> char[]
char[] dots = new char[fontHeight * fontWidth];
for (int i = 0; i < fontData.length; i++) {
byte b = fontData[i];
for (int bit = 7; bit >= 0; bit--) {
int bitValue = (b >> bit) & 1;
dots[i * 8 + (7 - bit)] = bitValue == 1 ? '1' : '0';
}
}
// char[] -> char[][] by fontWidth
char[][] dotMat = new char[fontHeight][fontWidth];
for (int i = 0; i < fontHeight; i++) {
System.arraycopy(dots, i * fontWidth, dotMat[i], 0, fontWidth);
}
return dotMat;
}

/**
* 加粗逻辑
*/
private char[] setBold(char[] dot, boolean fontBold) {
if (!fontBold) {
return dot;
}

char[] result = new char[dot.length];
for (int i = 0; i < dot.length; i++) {
char current = dot[i];
char shifted = i == 0 ? '0' : dot[i - 1]; // 前一个字符,用'0'补齐
result[i] = (current == '1' || shifted == '1') ? '1' : '0';
}

return result;
}

/**
* 字间距逻辑
*/
private char[] setSpacing(char[] dotArray, int spacing) {
if (spacing <= 0) {
return dotArray; // 如果 spacing <= 0,直接返回原数组
}

char[] result = new char[dotArray.length + spacing];
int index = 0;

for (char c : dotArray) {
result[index++] = c;
}

// 添加字间距
for (int i = 0; i < spacing; i++) {
result[index++] = '0';
}

return result;
}

/**
* 对齐点阵尾部
*/
private void padDotMat(char[][] dotMat) {
// 先获取最长的长度
int maxRowLength = 0;
for (char[] row : dotMat) {
maxRowLength = Math.max(maxRowLength, row.length);
}
// dotMat根据最长的长度补齐尾部
for (int k = 0; k < dotMat.length; k++) {
char[] row = dotMat[k];
char[] newRow = new char[maxRowLength];
System.arraycopy(row, 0, newRow, 0, row.length);
for (int j = row.length; j < maxRowLength; j++) {
newRow[j] = '0';
}
dotMat[k] = newRow;
}
}

/**
* 合并多个点阵
*/
private char[][] mergeDotMat(ArrayList<char[][]> dotMatList) {
// 计算最终宽度
int finalWidth = this.calcFinalWidth(dotMatList);
int fontHeight = this.fontHeight;
char[][] finalDotMat = new char[fontHeight][finalWidth];
int columnIndex = 0;
for (char[][] chars : dotMatList) {
int rowIndex = 0;
for (char[] aChar : chars) {
System.arraycopy(aChar, 0, finalDotMat[rowIndex++], columnIndex, aChar.length);
}
columnIndex += chars[0].length;
}
return finalDotMat;
}

/**
* 计算最终宽度
*/
private int calcFinalWidth(ArrayList<char[][]> dotMatList) {
int finalWidth = 0;
for (char[][] dotMat : dotMatList) {
finalWidth += dotMat[0].length;
}
return finalWidth;
}

/**
* 将dotMat绘制到图像
*/
private void renderDotMatToImage(char[][] dotMat, BufferedImage image, int x, int y) {
for (int j = 0; j < dotMat.length; j++) {
char[] row = dotMat[j];
for (int k = 0; k < row.length; k++) {
char bitValue = dotMat[j][k];
image.setRGB(x + k, y + j, bitValue == '1' ? Color.BLACK.getRGB() : Color.WHITE.getRGB());
}
}
}

加载/读取字体数据相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private final Map<String, byte[]> fontFileDataMap = new ConcurrentHashMap<>();

private byte[] readFontData(String fontFileName, int position, int byteCount) throws IOException {
byte[] fontFileData = fontFileDataMap.get(fontFileName);
if (fontFileData == null) {
fontFileData = this.loadFontFileData(fontFileName);
}

byte[] fontData = new byte[byteCount];
System.arraycopy(fontFileData, position, fontData, 0, byteCount);
return fontData;
}

private synchronized byte[] loadFontFileData(String resourcePath) throws IOException {
// 检查并发场景下其他线程是否已加载,避免重复加载
byte[] fontFileData = fontFileDataMap.get(fontFileName);
if (fontFileData != null) {
return fontFileData;
}

try (InputStream inputStream = LatticeText.class.getClassLoader().getResourceAsStream(resourcePath);
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
if (inputStream == null) {
throw new IOException("Resource not found: " + resourcePath);
}

byte[] data = new byte[1024];
int bytesRead;

// Read the input stream into the byte array output stream
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}

// Return the byte array
fontFileData = buffer.toByteArray();
fontFileDataMap.put(fontFileName, fontFileData);
return fontFileData;
}
}

以上代码省略了属性和部分方法,主要流程如下:
1.遍历文本,准备获取每个字符的点阵数据
2.获取字符码点
3.根据码点和每个字符点阵数据的字节数,计算在字体文件中的位置
4.读取/加载字体文件/数据
5.将字节数组转换为点阵多维数组,每个比特用一个char(‘0’|’1’)表示
6.根据码点值确认中英文,再处理加粗、间距等逻辑
7.补齐点阵数据宽度,加入集合
8.向右合并所有点阵数据,生成最终点阵数据
9.将点阵数据绘制到图像并返回

遇到的问题:
由于打包后文件布局发生变化,目标文件是在jar包中而不是直接在文件系统中,所以无法直接使用RandomAccessFile读取;
解决:
直接以resource方式读取字体数据到内存并缓存;

效果示例:
点阵图

参考资料#

使用FFmpeg获取音频时长

记于:2024-05-06 晚
地点:浙江省·温州市·家里
天气:多云

背景#

业务需求上需要使用ffmpeg转码并获取其识别的音频时长。

代码#

命令示例:

1
ffmpeg -y -hide_banner -i input.wav output.wav

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[aist#0:0/pcm_s16le @ 0x122f06af0] Guessed Channel Layout: mono
Input #0, wav, from 'input.wav':
Metadata:
encoder : Lavf61.1.100
Duration: 00:00:07.86, bitrate: 256 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 16000 Hz, mono, s16, 256 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'output.wav':
Metadata:
ISFT : Lavf61.1.100
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 16000 Hz, mono, s16, 256 kb/s
Metadata:
encoder : Lavc61.3.100 pcm_s16le
[out#0/wav @ 0x6000002c8180] video:0KiB audio:246KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 0.031011%
size= 246KiB time=00:00:07.86 bitrate= 256.1kbits/s speed=7.04e+03x

Duration: 00:00:07.86 即为音频时长。

接下来使用Java代码调用 ffmpeg 命令,并解析输出信息获取音频时长。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 转码并返回音频时长
*/
public Double transcoding(String ffmpegPath, String input, String output) {
String info = output + ".info";
FileUtil.touch(info);
String[] ffmpegCommand = {
ffmpegPath,
"-hide_banner",
"-y",
// ... 其他参数
"-i", input, output,
"2>&1 | cat >", info
};
List<String> listCommand = CollUtil.newArrayList(ffmpegCommand);

String finalCommandStr = StrUtil.join(" ", listCommand);
log.info("finalCommandStr: {}", finalCommandStr);

int exitCode = -1;
try {
// ProcessBuilder pb = new ProcessBuilder(listCommand);
// 如果有重定向的逻辑,一定要这样调用!!!
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", String.join(" ", listCommand));
Process process = pb.inheritIO().start();
exitCode = process.waitFor();
process.destroy();
} catch (Exception e) {
e.printStackTrace();
}

log.info("exitCode: {}", exitCode);

// 获取音频时长
Double length = this.getAudioLength(info);
FileUtil.del(info);

return length;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 获取音频时长
*/
private Double getAudioLength(String infoFile) {
Double durationInSeconds = null; // 初始化为 null

// 读取infoFile,提取音频时长信息
List<String> lines = FileUtil.readLines(infoFile, "UTF-8");
for (String line : lines) {
if (line.contains("Duration:")) {
// 提取包含 "Duration:" 的行
String durationLine = line.trim();
String durationPart = durationLine.split("Duration:")[1].trim().split(",")[0].trim();

// 解析时长信息,转换为 Double,精确到毫秒
String[] timeParts = durationPart.split(":");
double hours = Double.parseDouble(timeParts[0]);
double minutes = Double.parseDouble(timeParts[1]);
double seconds = Double.parseDouble(timeParts[2]);
double milliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000;

durationInSeconds = milliseconds / 1000.0; // 转换为秒
}
}
return durationInSeconds;
}

注意:

  • ffmpeg命令的输出信息是输出到标准错误流的,所以将标准错误重定向到标准输出流,然后通过管道配合cat命令将标准输出流输出到文件中;(尝试过使用process.getOutputStream()和process.getErrorStream(),但是无法获取到输出信息,所以使用了重定向的方式)
  • 输出文件的中间目录如果不存在,需要提前创建;

补充(2024-05-20):ffprobe命令专门用于检测多媒体文件的信息。

JavaJNI调用C/C++代码

记于:2024-04-14
地点:浙江省·温州市·家里
天气:阴雨

背景#

因为一个项目需要Java JNI调用C++ SDK,只有编译好的Windows DLL,没有对应的源码。

测试#

简单demo#

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
try {
String path = "C:\\Users\\ysd\\IdeaProjects\\test\\dll";
// 无效
// System.setProperty("java.library.path", path);
// 需要使用这种方式
// 参考:https://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-runtime.html
addLibraryPath(path);
} catch (Exception e) {
throw new RuntimeException(e);
}

System.loadLibrary("jnitest");
}

/**
* Adds the specified path to the java library path
*
* @param pathToAdd the path to add
* @throws Exception
*/
public static void addLibraryPath(String pathToAdd) throws Exception {
final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
usrPathsField.setAccessible(true);

// get array of paths
final String[] paths = (String[]) usrPathsField.get(null);

// check if the path to add is already present
for (String path : paths) {
if (path.equals(pathToAdd)) {
return;
}
}

//add the new path
final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = pathToAdd;
usrPathsField.set(null, newPaths);
}

// ================================================================================

// 本地方法
private static native String sayHello(String name);

public static void main(String[] args) {
String result = sayHello("ysm");
System.out.println("result: " + result);
}
}

生成本地方法对应的C++代码的头部文件;
可以使用javah或javac(jdk版本>=9)命令;
com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

接下需要根据头部文件中函数定义进行实现
JniTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "com_yeshimin_test_jni_JniTest.h"
// <jni.h>里已包含<stdio.h>

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
# 我的自定义dll目录为C:\Users\ysd\IdeaProjects\test\dll
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTest.c

# jni需要jni.h相关头部,这里需要指定头部所在目录;具体位置因系统和jdk版本而异
# -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32"

运行

1
result: Hello, ysm

不同包下#

场景为,调用点所在的包(和类)与目标本地代码的声明不一致时,调用会报错;
!!!重要前提,场景假设为,目标代码是编译好的dll;直接源码编译到一起的不算~

注意,JniTest.java和JniTestInternal.c是在不同包下,JniTestInternal是目标代码,JniTest是调用类

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitestinternal");
}

// 本地方法
private static native String sayHelloInternal(String name);

public static void main(String[] args) {
String result = sayHelloInternal("ysm");
System.out.println("result: " + result);
}
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

运行

1
result: Hello, ysm

如果将类名修改,使得调用点所在包(和类名)与目标定义不一致,比如将类名修改为”OtherName”,编译运行后,将会报错:

1
2
3
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.sample.OtherName.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at org.sample.OtherName.sayHelloInternal(Native Method)
at org.sample.OtherName.main(OtherName.java:60)

解决方案
使用“代理”的方式调用目标方法;即在同包名(和类名)对应的本地实现中包含目标头部,并且封装一个方法进行目标调用;具体见下面代码

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native String sayHello(String name);

private static native String sayHelloByProxy(String name);

private static native String sayHelloInternal(String name);

public static void main(String[] args) {

// 调用同包名下的本地方法;执行成功
String result = sayHello("ysm");
System.out.println("result: " + result);

// “代理”方式下,调用不同包名下的本地方法;执行成功
String result2 = sayHelloByProxy("ysm");
System.out.println("result2: " + result2);

// 调用不同包名下的本地方法;执行报错
String result3 = sayHelloInternal("ysm");
System.out.println("result3: " + result3);
}
}

com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *env, jclass cls, jstring j_str) {

return Java_org_sample_JniTestInternal_sayHelloInternal(env, cls, j_str);
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行

1
2
3
4
5
6
7
result: Hello, ysm

result2: Hello, ysm

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.yeshimin.test.jni.JniTest.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at com.yeshimin.test.jni.JniTest.sayHelloInternal(Native Method)
at com.yeshimin.test.jni.JniTest.main(JniTest.java:69)

调用dll#

场景跟上面【不同包下】类似,区别为目标是第三方sdk中的dll文件,且没有头部文件

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native int dynamicInvoke();

public static void main(String[] args) {
int intResult = dynamicInvoke();
System.out.println("intResult: " + intResult);
}
}

com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

JniTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <windows.h>
#include <stdbool.h>
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *env, jclass cls) {

typedef int (__stdcall *TargetMethodPtr)(); // 声明 TargetMethodPtr 类型,名称自定义
TargetMethodPtr targetMethod;

HMODULE hModule = LoadLibrary(TEXT("Log_MD_VC120_v3_1.dll")); // 加载DLL
if (hModule != NULL) {
targetMethod = (TargetMethodPtr)GetProcAddress(hModule, "?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ"); // 获取函数地址
if (targetMethod != NULL) {
printf("function found");
bool result = targetMethod(); // 调用DLL函数
FreeLibrary(hModule); // 释放DLL模块
return result ? 1 : 0;
} else {
printf("function not found");
}
FreeLibrary(hModule); // 释放DLL模块
} else {
// getLastError
DWORD dwError = GetLastError();
printf("Error code: %lu\n", dwError);
}
return -1; // 返回 NULL 表示失败
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行
将上面两个dll以及用到的第三方dll及其依赖dll放到项目根目录下才行正常运行,原因未知,见【遇到的问题】第3点

1
intResult: 1

遇到的问题#

1.提示32位代码在64位架构下跑不通;是下载错了mingw架构,应该下载64位的
2.指定自定义dll目录无效;解决方法见上面【简单demo】代码
3.在【调用dll】测试过程中,dll文件放到自定义dll目录下,链接不到,放到项目根目录下就可以,暂时搞不清楚
4.在【调用dll】测试过程中,一直获取不到目标函数;
开始是通过dllexp工具查看的函数函数名称为 public: static bool __cdecl GenICam_3_1::CLog::ConfigureFromEnvironment(void),
也试过 GenICam_3_1::CLog::ConfigureFromEnvironment(void) 和 GenICam_3_1::CLog::ConfigureFromEnvironment,都不行
后来是通过visual studio的dumpbin工具查看到dll的正确函数名 ?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ

参考资料#

腾讯企业自定义域名邮箱发送邮件到谷歌Gmail失败

记于:2024-04-10 晚上
地点:浙江省·温州市·家里
天气:晴天

背景#

我的邮箱域名是绑定到腾讯企业邮箱的,今天发了一封邮件到一个gmail邮箱,被退回了,退信原因如下:


很抱歉您发送的邮件被退回,以下是该邮件的相关信息:
…省略…
退信原因 发件人(…省略…)域名的DNS记录未设置或设置错误导致对方拒收此邮件。
host gmail-smtp-in.l.google.com[74.125.23.26] said: 550-5.7.26 This mail has been blocked because the sender is unauthenticated. Gmail requires all senders to authenticate with either SPF or DKIM. Authentication results: DKIM = did not pass SPF [yeshimin.com] with ip: [54.206.16.166] = did not pass For instructions on setting up authentication, go to https://support.google.com/mail/answer/81126#authentication y23-20020a17090264d700b001e3d3ac40a1si8808240pli.17 - gsmtp (in reply to end of DATA command)
解决方案 请通知你的邮箱管理员为邮箱域名设置正确的DNS(SPF、DKIM、DMARC)记录。详细请见 http://service.exmail.qq.com/cgi-bin/help?subtype=1&&no=1000580&&id=20012。

此外,您还可以 点击这里 获取更多关于退信的帮助信息。


根据以上信息得知,发现是因为邮箱域名缺少SPF、DKIM、DMARC记录导致的,所以需要在域名解析中添加这些记录。

解决过程#

1.添加SPF记录和DMARC记录#

参考:https://open.work.weixin.qq.com/help2/pc/19820?person_id=1&subtype=1&id=29&no=188

1.1 添加SPF记录#

添加一条TXT记录,主机名为@,记录值为:v=spf1 include:spf.mail.qq.com ~all

1.2 添加DMARC记录#

添加一条TXT记录,主机名为_dmarc,记录值为:v=DMARC1; p=none; rua=mailto:mailauth-reports@qq.com

2.添加DKIM记录#

2.1 以管理员身份进入腾讯企业邮箱后台
2.2 菜单【工具箱】 -> 【DKIM验证】
2.3 根据配置信息,在域名解析中添加相应TXT记录即可

微信小程序-同步登陆方案

记于:2024-03-11 上午
地点:浙江省·温州市·家里
天气:下雨

背景#

业务流程上需要等微信登陆(wx.login)后再执行后续操作,但是wx.login是异步的,如果简单地使用回调的方式,代码将会不简洁,同时也为了职责分离(将登陆与业务逻辑解耦),所以需要一个同步的方案。

思路#

根据网上的一些方案,比如使用Promise、async/await等,试过都失败了(可能是因为对这些技术不太熟);
最终选择从业务流程上入手,加一个加载页可以解决;
具体地:添加一个加载/入口(entry)页作为第一页面,在其中执行wx.login,成功则跳转到首页,如果失败则停留在加载页,并提示点击重新登陆。

代码#

entry.wxml

1
2
<!--pages/entry/entry.wxml-->
<button wx:if="{{initFail}}" bind:tap="onRetryTap" style="margin-top: 200px;">点击重试</button>

entry.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// pages/entry/entry.js
const consts = require('@/common/consts/consts.js')
const https = require('@/common/utils/https.js')
const base64 = require('@/common/utils/base64.js')
Page({
/**
* 页面的初始数据
*/
data: {
initFail: false,
userId: undefined
},
// data
// ================================================================================
// lifecycle
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('entry.onLoad: {}', JSON.stringify(options))
this.init()
},
// ================================================================================
// methods

// 初始化
init() {
console.log('init...')

var that = this

that.setData({ initFail: false })

wx.showLoading({ title: '初始化...', mask: true })

// 静默登录
wx.login({
success: (res) => {
console.log('wx.login.success: ' + JSON.stringify(res))
if (res.code) {
// 执行业务登录
https.post({
url: consts.apis.WXMP_LOGIN,
data: { code: res.code },
ignoreAuth: true,
success: function (res) {
if (res.data.data && res.data.data.token) {
var token = res.data.data.token
var userId = that.parseUserId(token)
// 保存到全局变量
getApp().globalData.token = token
getApp().globalData.userId = userId
// 保存到本地
wx.setStorageSync('token', token)
wx.setStorageSync('userId', userId)

// 跳转到首页
that.jumpToIndex()
} else {
that.setData({initFail: true})
}
},
fail: function(e) {
console.log('wxLogin.fail: {}', JSON.stringify(e))
that.setData({initFail: true})
},
complete: function(e) {
console.log('wxLogin.complete: {}', JSON.stringify(e))
wx.hideLoading()
}
})
} else {
console.log('登录失败!' + res.errMsg)
that.setData({initFail: true})
}
},
})
},
// 跳转到首页
jumpToIndex(options) {
console.log('jumpToIndex...{}', JSON.stringify(options))
wx.switchTab({ url: '/pages/index/index' })
},
// 当点击重试
onRetryTap(e) {
console.log('onRetryTap...')
this.init()
},
// 解析用户ID
parseUserId(token) {
// ...
}
})

微信小程序-自定义导航栏组件

记于:2024-03-05 晚上
地点:浙江省·温州市·家里
天气:多云

背景#

微信官方提供的navigation-bar组件集成后布局有些错位,一直调整不好,索性自定义一个。

目标#

布局可控、功能可控即可;
目前主要是需要一个中间的标题和左边的一个返回;
然后布局上适配微信小程序的胶囊按钮;
如图:
导航栏

难点与思路#

唯一算是难点的地方就是和胶囊按钮的适配,也就是要达到让胶囊按钮在导航栏内垂直方向居中的效果;
参考了下网上的一些方案,思路大同小异,主要是通过系统状态栏和胶囊按钮的布局参数来计算导航栏的高度和位置;
首先在app.js中获取计算所需的信息,然后计算出导航栏参数,最终在使用的页面中根据参数渲染出来。

代码#

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// app.js
App({
globalData: {
// 系统信息和胶囊信息
systemInfo: undefined,
menuButton: undefined,
// 状态栏信息
statusBarInfo: {
sysBottom: undefined,
sysHeight: undefined,
appBottom: undefined,
appHeight: undefined
}
},
onLaunch(options) {
console.log('app.onLaunch...{}', JSON.stringify(options))

this.globalData.systemInfo = wx.getSystemInfoSync()
// console.log('systemInfo...', JSON.stringify(this.globalData.systemInfo))
this.globalData.menuButton = wx.getMenuButtonBoundingClientRect()
// console.log('menuButton...', JSON.stringify(this.globalData.menuButton))

// 计算状态栏信息
this.globalData.statusBarInfo.sysHeight = this.globalData.systemInfo.statusBarHeight
this.globalData.statusBarInfo.sysBottom = this.globalData.systemInfo.screenTop + this.globalData.systemInfo.statusBarHeight

this.globalData.statusBarInfo.appHeight = (((this.globalData.menuButton.bottom + this.globalData.menuButton.top) / 2) - this.globalData.systemInfo.statusBarHeight) * 2
this.globalData.statusBarInfo.appBottom = this.globalData.statusBarInfo.sysBottom + this.globalData.statusBarInfo.appHeight
console.log('statusBarInfo...', JSON.stringify(this.globalData.statusBarInfo))
}
})

custom-navigation-bar.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// components/custom-navigation-bar/custom-navigation-bar.js
Component({
/**
* 组件的属性列表
*/
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
},
title: {
type: String,
value: undefined
},
back: {
type: Boolean,
value: false
},
backgroundColor: {
type: String,
value: 'none'
}
},
/**
* 组件的初始数据
*/
data: {
// 状态栏信息
statusBarInfo: {
sysBottom: undefined,
sysHeight: undefined,
appBottom: undefined,
appHeight: undefined,
title: undefined,
back: false,
backgroundColor: 'none'
}
},
lifetimes: {
/**
* 在组件实例进入页面节点树时执行
*/
attached: function() {
console.log('custom-navigation-bar.lifetimes.attached...')

// 计算
this.calc()
},
/**
* 在组件实例被从页面节点树移除时执行
*/
detached: function() {
console.log('custom-navigation-bar.lifetimes.detached...')
},
},
/**
* 组件的方法列表
*/
methods: {
/**
* 计算
*/
calc: function () {
console.log('custom-nav.calc...')
// 设置状态栏信息
var statusBarInfo = getApp().globalData.statusBarInfo
statusBarInfo.title = this.properties.title
statusBarInfo.back = this.properties.back
// 背景颜色
statusBarInfo.backgroundColor = this.properties.backgroundColor
this.setData({ statusBarInfo })
console.log('statusBarInfo...', JSON.stringify(this.data.statusBarInfo))
},
onBackTap(e) {
console.log('onBackTap...')
wx.navigateBack()
}
}
})

custom-navigation-bar.json

1
2
3
4
{
"component": true,
"usingComponents": {}
}

custom-navigation-bar.wxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 这是自定义组件的内部WXML结构 -->
<!-- 状态栏相关 -->
<view class="status-bar" style="background-color: {{backgroundColor}};">
<!-- 系统状态栏占位 -->
<view class="system-status-bar" style="height: {{statusBarInfo.sysHeight}}px;"></view>
<!-- 应用状态栏-->
<view class="app-status-bar" style="height: {{statusBarInfo.appHeight}}px;">
<!-- 左侧按钮 -->
<view class="app-status-bar-left">
<view wx:if="{{statusBarInfo.back}}" class="back" bind:tap="onBackTap">返回</view>
</view>
<!-- 中间部分 -->
<view class="app-status-bar-center">
<!-- 标题 -->
<view class="title">{{statusBarInfo.title}}</view>
</view>
<!-- 右侧占位 -->
<view class="app-status-bar-right"></view>
</view>
</view>

<slot></slot>

custom-navigation-bar.wxss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* components/custom-navigation-bar/custom-navigation-bar.wxss */
/* 这里的样式只应用于这个自定义组件 */
.app-status-bar {
display: flex;
align-items: center;
}
.app-status-bar-left {
flex: 1;
display: flex;
}
.app-status-bar-center {
flex: 1;
display: flex;
justify-content: center;
}
.app-status-bar-right {
flex: 1;
}

.back {
padding-top: 6px;
padding-bottom: 6px;
padding-left: 8px;
margin-left: 4px;
padding-right: 32px;
}

使用示例

1
2
3
4
<!--pages/mine-edit/mine-edit.wxml-->
<custom-navigation-bar title="编辑" back="{{true}}"></custom-navigation-bar>
<scroll-view class="content-container" scroll-y="true" enable-flex="true">
</scroll-view>

同时需要在使用的页面对应的json配置文件中添加引用

1
2
3
4
5
{
"usingComponents": {
"custom-navigation-bar": "/components/custom-navigation-bar/custom-navigation-bar"
}
}

以及在app.json中添加配置

1
2
3
4
5
{
"window": {
"navigationStyle": "custom"
}
}

Python爬虫-百度热搜

记于:2023-11-26 上午
地点:浙江省·温州市·家里
天气:晴天

背景#

最近在学习大数据技术,需要一些数据实践一下,正好又想做个新闻类的个人服务/app,所以先抓取一些新闻类数据;
虽然也有不少公开的数据集,但还是想自己抓取下,顺便练下爬虫技术。

代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/python
# -*- coding: utf-8 -*-

# ------------------------------------------------------------------------------
# import

from datetime import datetime
from bs4 import BeautifulSoup, Comment
from logging.handlers import TimedRotatingFileHandler
import logging
import requests
import json
import os

# ------------------------------------------------------------------------------
# define

# 定义
datetime_text = datetime.now().strftime('%Y%m%d_%H%M%S_%s')
date_text = datetime.now().strftime('%Y%m%d')

DATA_DIR = '/var/lib/ysmspace-crawler/data/'
LOG_DIR = '/var/log/ysmspace-crawler/'
RESPONSE_DATA_FILE = DATA_DIR + 'response.{}.html'.format(datetime_text)
PURE_DATA_FILE = DATA_DIR + 'data.{}.txt'.format(datetime_text)
LOG_FILE = LOG_DIR + 'message.{}.log'.format(date_text)

SDATA_STARTS = 's-data:'

# ------------------------------------------------------------------------------
# init

# 目录
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(LOG_DIR, exist_ok=True)

# 日志
log = logging.getLogger(__name__)
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

# 输出基本信息
log.info('define.datetime_text: %s', datetime_text)
log.info('define.date_text: %s', date_text)
log.info('define.DATA_DIR: %s', DATA_DIR)
log.info('define.LOG_DIR: %s', LOG_DIR)
log.info('define.RESPONSE_DATA_FILE: %s', RESPONSE_DATA_FILE)
log.info('define.PURE_DATA_FILE: %s', PURE_DATA_FILE)
log.info('define.LOG_FILE: %s', LOG_FILE)

log.info('init finish...')

# ------------------------------------------------------------------------------
# main

# 请求原始数据
url = 'https://top.baidu.com/board?tab=realtime'
log.info('response.url: %s', url)
response = requests.get(url)
log.info('response.status_code: %s', response.status_code)
with open(RESPONSE_DATA_FILE, 'w') as file:
file.write(response.text)
log.info('write response.text to %s', RESPONSE_DATA_FILE)

# 检查请求是否成功
if response.status_code == 200:
log.info('request success')
# 解析HTML内容
soup = BeautifulSoup(response.text, 'html.parser')

# 获取包含数据的注释
comment_sdata = soup.find_all(string=lambda text: isinstance(text, Comment) and text.strip().startswith(SDATA_STARTS))
# 提取数据
data_text = comment_sdata[0][len(SDATA_STARTS):]
log.info('parse data success')
with open(PURE_DATA_FILE, 'w') as file:
file.write(data_text)
log.info('write data to %s', PURE_DATA_FILE)
else:
log.info('request failed')

log.info('--------------------------------------------------------------------------------')

以上为第一版代码,后续可能进行优化,比如输出文件按日期分组到目录;

执行与输出#

执行

1
# python baidu-realtime.py

查看日志文件

1
# ll /var/log/ysmspace-crawler
1
2
total 2920
-rw-r--r-- 1 yeshimin wheel 414990 11 26 10:58 message.20231126.log

查看数据文件

1
# ll /var/lib/ysmspace-crawler/data
1
2
-rw-r--r--  1 yeshimin  wheel  45522 11 26 10:58 data.20231126_105801_1700967481.txt
-rw-r--r-- 1 yeshimin wheel 194748 11 26 10:58 response.20231126_105801_1700967481.html

补充#

微博热搜抓取代码逻辑类似

Java集成FFmpeg实现音频转码

记于:2023-10-24 下午
地点:浙江省·温州市·家里
天气:晴天

背景#

Java项目上的一个需求,需要对各种格式的音频文件进行转码,输出为mp3格式;
在了解到可选实现方案有【阿里云媒体处理MPS】和【自己使用FFmpeg】后,优先选择前者进行调研与测试;
在一番配置之后,【阿里云媒体处理MPS】能够成功实现自动转码,但是转码后的文件始终不能在项目的硬件设备中播放;
比对项目要求的转码参数:采样率-16kHz,单声道,比特率-32k,并且需要是CBR固定码率的模式;
注意到【阿里云媒体处理MPS】自动转码任务配置中,仅支持【ABR平均码率模式】,怀疑是这个导致(此时还没有找到合适的查看参数的工具(mediainfo));
在询问客服之后,得到的答复是,可以尝试手动调用API并指定相关参数的方式;
我是觉得手动调用API的方式比较麻烦,而且转码还要费用,索性选择自己使用FFmpeg进行转码;

尝试FFmpeg#

先是测试ffmpeg命令转码的方式;
首先指定了基本参数:
ffmpeg -hide_banner -i input.wav -ar 16000 -ab 32k -ac 1 output.mp3
输出文件在macos的【音乐】软件下可正常播放,但是查看其信息时发现码率始终为40k,与指定的32k参数不符;
在尝试各种参数及组合之后,始终不正确;
尝试参数大概有:

1
2
3
4
5
-vn 仅输出音频流
-b:a 32k 以不同形式指定码率
-c:a libmp3lame 明确指定mp3编码器
-af "volume=1" 指定音量不变
-map_metadata -1 -fflags +bitexact -flags:a +bitexact 各种去除元信息的参数

在各种尝试无果后,无意间发现一篇博客,标题为【解决ffmpeg生成mp3在ios上时长不对的问题】;
在博客中,作者提到了一个参数:-write_xing 0,同时发现这个问题是一个bug(见参考资料);
在添加了-write_xing 0参数后,转码成功,输出文件的码率也正确为32k(可以使用mediainfo查看);

最终命令为:
ffmpeg -hide_banner -i input.wav -ar 16000 -ab 32k -ac 1 -write_xing 0 -map_metadata -1 -fflags +bitexact -flags:a +bitexact -c:a libmp3lame output.mp3

参数解释如下:

1
2
3
4
5
6
7
8
9
10
11
`-hide_banner`: 隐藏 FFmpeg 的标志栏
`-i input.wav`: 指定输入文件为 "input.wav"
`-ar 16000`: 设置音频采样率为 16,000 Hz
`-ab 32k`: 设置音频比特率为 32 kbps(千位每秒)
`-ac 1`: 设置输出音频为单声道(单通道)
`-write_xing 0`: 禁用写入 Xing VBR 头部信息
`-map_metadata -1`: 删除输入文件的元数据(如标签信息)
`-fflags +bitexact`: 设置输入文件的比特精度标志
`-flags:a +bitexact`: 设置输出文件的音频比特精度标志
`-c:a libmp3lame`: 使用 libmp3lame 编码器来进行 MP3 压缩
`output.mp3`: 指定输出文件名为 "output.mp3"

Java集成#

发现没有直接用Java实现的ffmpeg库,要么是调用本地ffmpeg命令,要么是使用jni;
发现一个叫做【javacv】的库,可以使用Java调用ffmpeg命令,进行尝试了,由于定位不到ffmpeg命令,所以放弃了,改用Java的ProcessBuilder方式;
核心代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    @Override
public int transcoding(String input, String output) throws IOException, InterruptedException {
String[] ffmpegCommand = {
"/usr/bin/ffmpeg", // linux
// "/opt/homebrew/bin/ffmpeg", // macos
"-hide_banner",
"-i", input,
"-ar", "16000",
"-ab", "32k",
"-ac", "1",
"-write_xing", "0",
"-map_metadata", "-1",
"-fflags", "+bitexact",
"-flags:a", "+bitexact",
"-c:a", "libmp3lame",
output
};

ProcessBuilder pb = new ProcessBuilder(ffmpegCommand);
int exitCode = pb.inheritIO().start().waitFor();
log.info("transcoding.exitCode: {}", exitCode);
return exitCode;
}

还有一个功能点,是获取音频文件的时长;
原来的思路是,调用ffmpeg输出音频文件信息,但是发现输出无法被捕获,经查询发现ffmpeg默认输出到了stderr,而不是stdout;
在连同stderr一起输出后,依然无法捕获,经过一番搜索与尝试,定位到问题在ProcessBuilder的重定向逻辑上;
如果有重定向的逻辑,调用方式为(/bin/sh):
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", String.join(" ", ffmpegCommand));
核心代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    private Double getAudioLength(String input) {
String infoFile = input + ".info";
String[] ffmpegCommand = {
"/usr/bin/ffmpeg", // linux
// "/opt/homebrew/bin/ffmpeg", // macos
"-hide_banner",
"-i", input,
"2>&1", "|", "cat",
">", infoFile
};

Double durationInSeconds = null; // 初始化为 null

try {
// ProcessBuilder pb = new ProcessBuilder(ffmpegCommand);
// 如果有重定向的逻辑,一定要这样调用!!!
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", String.join(" ", ffmpegCommand));
Process process = pb.inheritIO().start();
int exitCode = process.waitFor();
log.info("exitCode: {}", exitCode);

// 读取infoFile,提取音频时长信息
List<String> lines = FileUtil.readLines(infoFile, "UTF-8");
for (String line : lines) {
if (line.contains("Duration:")) {
// 提取包含 "Duration:" 的行
String durationLine = line.trim();
String durationPart = durationLine.split("Duration:")[1].trim().split(",")[0].trim();

// 解析时长信息,转换为 Double,精确到毫秒
String[] timeParts = durationPart.split(":");
double hours = Double.parseDouble(timeParts[0]);
double minutes = Double.parseDouble(timeParts[1]);
double seconds = Double.parseDouble(timeParts[2]);
double milliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000;

durationInSeconds = milliseconds / 1000.0; // 转换为秒
}
}

process.destroy();

if (durationInSeconds != null) {
log.info("音频时长(秒,精确到毫秒):{}", durationInSeconds);
} else {
log.info("未找到音频时长信息。");
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
FileUtil.del(infoFile);
}

return durationInSeconds;
}

Docker部署#

由于项目在非本地环境是以Docker形式部署,所以又出现问题了;
ffmpeg命令安装的问题,如果是安装到宿主机上,担心跟环境绑定得太死;
所以选择使用Dockerfile将其安装到容器中;
但是使用原镜像(pig4cloud/java:8-jre)无法直接安装ffmpeg;
经过一番搜索与测试,最终使用(openjdk:8-jdk-alpine)镜像,同时配置源,ffmpeg安装成功;
Dockerfile示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM openjdk:8-jdk-alpine

MAINTAINER yeshimin

ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"

RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN mkdir -p /xxx-module

RUN mkdir -p /tmp/xxx-audio

RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \
&& apk update upgrade \
&& apk add --no-cache procps unzip curl bash tzdata \
&& apk add yasm && apk add ffmpeg \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone

WORKDIR /xxx-module

EXPOSE 10003

ADD ./target/xxx-module-biz.jar ./

CMD java $JAVA_OPTS -jar xxx-module-biz.jar

控制输出文件大小#

要求:在采样率、码率、声道数固定的情况下,控制输出文件的大小不超过60K;
经过搜索,可以使用-fs参数来控制输出文件的大小;
但是实际执行之后会报错:

1
2
3
# ffmpeg -hide_banner -i caiqin.wav -ar 16000 -ab 32k -ac 1 \
-write_xing 0 -map_metadata -1 -fflags +bitexact -flags:a +bitexact \
-c:a libmp3lame -fs 60k caiqin.mp3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, wav, from 'caiqin.wav':
Duration: 00:50:07.91, bitrate: 1411 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, 2 channels, s16, 1411 kb/s
File 'caiqin.mp3' already exists. Overwrite? [y/N] y
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'caiqin.mp3':
Stream #0:0: Audio: mp3, 16000 Hz, mono, s16p, 32 kb/s
Metadata:
encoder : Lavc libmp3lame
[out#0/mp3 @ 0x6000016b80c0] Error muxing a packet.0kbits/s speed=N/A
size= 59kB time=00:00:15.15 bitrate= 31.7kbits/s speed= 307x
video:0kB audio:59kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.033307%
Conversion failed!

根据输出的第14行信息,应该是特定的采样率、码率、声道数下,转码后所需的文件大小超出指定的60K(展示为59kB)所导致;

在对时长进行控制后(使用-t参数),可以正常转码:

1
2
3
# ffmpeg -hide_banner -i caiqin.wav -ar 16000 -ab 32k -ac 1 \
-write_xing 0 -map_metadata -1 -fflags +bitexact -flags:a +bitexact \
-c:a libmp3lame -fs 60k -t 14 caiqin.mp3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, wav, from 'caiqin.wav':
Duration: 00:50:07.91, bitrate: 1411 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, 2 channels, s16, 1411 kb/s
File 'caiqin.mp3' already exists. Overwrite? [y/N] y
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'caiqin.mp3':
Stream #0:0: Audio: mp3, 16000 Hz, mono, s16p, 32 kb/s
Metadata:
encoder : Lavc libmp3lame
size= 55kB time=00:00:13.97 bitrate= 32.3kbits/s speed= 230x
video:0kB audio:55kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.035521%

在以上方案实现后,偶然发现ffmpeg指定输出文件大小后,虽然转码超过指定大小时依然会报错,但还是会自动截断并生成指定大小的文件;
所以不需要像上面方案那么绕了,直接执行一次,结果就满足了,只是exitCode返回非0而已;

问题处理-mp3文件附带封面导致指定大小参数失效#

在对某个音频文件进行转码时,发现转码后的文件大小没有被自动截断,执行信息如下:

1
2
3
# ffmpeg -hide_banner -i xx.mp3 -ar 16000 -ab 32k -ac 1 \
-write_xing 0 -map_metadata -1 -fflags +bitexact -flags:a +bitexact \
-c:a libmp3lame -fs 60k yy60k.mp3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Input #0, mp3, from 'xx.mp3':
Metadata:
encoder : Lavf58.76.100
comment : 163 key(Don't modify):cEgtqHxwpdRKcBCRPBlPkg2DgcBlNoDaE7DLI/2UHqz2lBjNbotyjPpbetGfYEpSHze+evmlulEGcCplT1L5nqhLHeK3XTcyEgnHwW9Ow1JY2+gbzf04bwvVKdfiIWN3LL/zdLfLxvlVsPvobu6nWgvQC6Nw7MGw5umsTeRPyYHy4w5YVSD8VHyFEo0eQKeaubW3qofyzOlJ0gYXQlMHN4RCn1jpkefyBGBCOcjKE
album : 大风遇到了雨
title : 不找了
artist : 隔壁老樊
album_artist : 隔壁老樊
disc : 01
track : 1
Duration: 00:04:11.81, start: 0.023021, bitrate: 128 kb/s
Stream #0:0: Audio: mp3, 48000 Hz, stereo, fltp, 128 kb/s
Metadata:
encoder : Lavc58.13
Stream #0:1: Video: mjpeg (Baseline), yuvj420p(pc, bt470bg/unknown/unknown), 1080x1920 [SAR 72:72 DAR 9:16], 90k tbr, 90k tbn (attached pic)
Metadata:
comment : Other
Stream mapping:
Stream #0:1 -> #0:0 (mjpeg (native) -> png (native))
Stream #0:0 -> #0:1 (mp3 (mp3float) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
[swscaler @ 0x128378000] deprecated pixel format used, make sure you did set range correctly
[swscaler @ 0x128378000] [swscaler @ 0x118008000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x128388000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x128398000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283a8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283b8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283c8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283d8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283e8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x128378000] [swscaler @ 0x1283f8000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] deprecated pixel format used, make sure you did set range correctly
[swscaler @ 0x110008000] [swscaler @ 0x118008000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118018000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118028000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118038000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118048000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118058000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118068000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118078000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x110008000] [swscaler @ 0x118088000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] deprecated pixel format used, make sure you did set range correctly
[swscaler @ 0x130008000] [swscaler @ 0x118088000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118008000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118018000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118028000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118038000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118048000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118058000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118068000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x130008000] [swscaler @ 0x118078000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] deprecated pixel format used, make sure you did set range correctly
[swscaler @ 0x118078000] [swscaler @ 0x118088000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118008000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118018000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118028000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118038000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118048000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118058000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118068000] No accelerated colorspace conversion found from yuv420p to rgb24.
[swscaler @ 0x118078000] [swscaler @ 0x118098000] No accelerated colorspace conversion found from yuv420p to rgb24.
[vost#0:0/png @ 0x126f0cef0] Frame rate very high for a muxer not efficiently supporting it.
Please consider specifying a lower framerate, a different muxer or setting vsync/fps_mode to vfr
Output #0, mp3, to 'yy60k.mp3':
Stream #0:0: Video: png, rgb24(pc, gbr/unknown/unknown, progressive), 1080x1920 [SAR 1:1 DAR 9:16], q=2-31, 200 kb/s, 90k fps, 90k tbn (attached pic)
Metadata:
comment : Other
encoder : Lavc png
Stream #0:1: Audio: mp3, 16000 Hz, mono, fltp, 32 kb/s
Metadata:
encoder : Lavc libmp3lame
[out#0/mp3 @ 0x6000017a80c0] Error muxing a packet02:27.60 bitrate= 0.0kbits/s speed= 288x
frame= 1 fps=0.0 q=-0.0 Lsize= 1061kB time=00:04:11.75 bitrate= 34.5kbits/s speed= 287x
video:78kB audio:983kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.003959%
Conversion failed!

可以看到里面有一个图片流:Stream #0:1 -> #0:0 (mjpeg (native) -> png (native)),怀疑是这个差异导致的;
一开始把图片也当作元信息,原来使用的参数中已经包含-map_metadata -1,用于去除元信息,为什么会无效;
查询了解到音频流中的封面图片属于数据流的一部分,不能算是常规的元信息;
然后使用了-map 0:a参数,指定只输出音频流,最终问题得到解决;

参考资料#

记·桂林游记

记于:2023-10-01 下午
地点:浙江省·温州市·家里
天气:阴雨

阿德出去长途旅游了,几天后我也出去了,09月26日于桂林汇合;
本来想着没做过飞机,正好可以坐一下飞机,而且看起来价格和高铁也差不多,
都是600上下,没想到机票付款页看到还要燃油费等费用,最终还是选择了高铁~
坐了8小时左右的高铁,晚上6点半左右到达桂林;
26日晚上住在伯曼酒店;
晚上出去买了点小吃,买多了,没吃完~

27日上午,去到一个古镇逛了逛,这里有竹筏漂流,80元一个人,没报;
这里有战斗机在天上飞,声音好大,手机拍不大清;
下午,游玩漓江,报了个团,300元一个人~然后去门店等待,11点半左右出发;
先坐大巴到岸边,一路上导游讲解、洗脑~讲广西/桂林经济不好,很不容易,为国家付出了很多,全国都欠它的,意思是要大家多消费~
第一个项目是坐竹筏,4人一组,没抢到前排,可惜;注意到竹筏居然都是金属的~后面装了个引擎~
上岸后准备吃饭,导游把队伍引导至一家饭店,说是附近只有这一家;
和阿德进去看了下菜单价格,就出来了,准备去周边再找找,导游还尝试劝说,生怕我们发现周边有更便宜的;
在门口有很多老奶奶推销水果、栗子等,我买了栗子,以免万一真找不到其他饭店~
没走多远就发现一家米粉店,只要10元一碗,还不错,就在这吃了;期间还买了一份水果;
吃完坐了一会,然后集合去坐游轮,游轮还是挺大的,没记错的话好像是四星级的;
然后就是吃吃栗子和水果,拍拍照,看看风景,偶尔船上导游讲解一下;
再次上岸后,又坐大巴,接下来是去一个少数民族寨子,应该是侗族;
有个寨子里的导游带着讲解,喝了米酒,刚入口感觉就是喝水,稍后上来一点酒气,之前没喝过;
最终往里走,就是卖银器的地方,没什么人,基本上就是这一团的人;
好说歹说反正我们就是不买,其他人有几个买的;里面逛了逛,待了挺久的,最终导游带着从另一条路线出去了;
然后就是坐大巴回程了,在车上导游分了点零食,以为只是简简单单分点零食,没想到最终还是推销~当然还是没买~
想起来旅游团居然没到阳朔,就是20元人民币上的风景地;
晚上到了某宾馆,出去吃了啤酒鱼,还不错,有点饱~

28日上午,准备去20元人民币上的风景地,因为前一天坐过竹筏,这次就不坐了,岸边拍了拍照;
接下来租了两辆电瓶车,30元每辆,押金100,车是改过的,表显最高25,实际上速度估计有50以上;随便逛,挺有意思的;
上午没什么太阳,很是凉快惬意,中午太阳出来了就有些热了;
中午吃的汉堡快餐,味道也不错;然后就开回去还车了;

然后就是准备上高速回家了,因为29日开始高速免费,怕堵车,所以早点回去;
全程高速1300多公里,原计划是两天开回去,中间在服务区休息一晚;
28日下午在休息区吃了面,然后继续出发,刚上路前方就发生车祸,堵了有半小时以上,一路上有多起大小车祸,所幸看起来没有人员伤亡;
后来阿德非要连夜开回去,总耗时18个小时左右,开车时间13个小时左右,大概是从下午1点多开到第二天早上6点多到家;
实在是危险,我一个坐车的,怕出事车上也没睡,真是坐车的比开车的都累,不过还好,最终平安到家了;
夜晚高速行车,也是不一样的一种体验,挺有意思的~后半夜路上车也比较少;

29日到家后,直接躺下就睡了,睡到中午。


某古镇
某古镇
竹筏山水
竹筏山水
竹筏山水
侗族寨子表演
侗族寨子银砖
侗族寨子银店
侗族寨子银匠
20元人民币风景地
20元人民币风景地