大道废

一切有为法,如梦幻泡影,如露亦如电,当作如是观.

0%


类别 名称 数量 重量 Check
三大件 田野睡袋 1 872 OK
therm-a-rest xlite+气泵 1 415 OK
AMK Bivy 地布 1 81 OK
山鹰户外帐篷 1 430 OK
hmg2400 1 1010 OK
钛空心地丁 10 100 OK
2908
炊具 火枫大黄蜂 1 40 OK
千橡树550ml锅 1 72 OK
purewell净水器 1 73 OK
2升鸭嘴兽 1 39 OK
勺子 1 2 OK
226
衣服 smartwood袜子 1 68
速干长裤 1 320
三峰防雨裙 1 69 OK
老鼠神衣 1 275 OK
始祖鸟冲锋衣 1 360 OK
羽绒服 1 330 OK
羊毛帽子 1 61 OK
buff太阳帽 1 30
羊毛内衣 1 205 OK
内裤 1 43 OK
1761
电器 anker一万毫安 1 203 OK
Apple Watch充电器 1 19 OK
紫米Type-C充电线+Lightning转接头+microUSB转接头 1 18 OK
Apple Watch 5 1 46 OK
iPhone 13 pro 1 205 OK
充电器 1 46 OK
头灯 1 40 OK
伏来阳太阳能板 1 195 OK
收纳袋 1 12 OK
784
杂项 牙刷牙膏毛巾耳塞 1 55 OK
眼镜 1 20 OK
药品 1 26 OK
迪卡侬雨伞 1 154 OK
5934 255

带的食物:

IMG_4469

分析的源码

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
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdio.h>
#include <stdlib.h>

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libswscale/swscale.h>
}

int main(int argc, char **argv)
{

// Open the initial context variables that are needed
SwsContext *img_convert_ctx;
AVFormatContext *format_ctx = avformat_alloc_context();
AVCodecContext *codec_ctx = NULL;
int video_stream_index;

// Register everything
//av_register_all();
// avformat_network_init();

// open RTSP
if (avformat_open_input(&format_ctx, "rtsp://134.169.178.187:8554/h264.3gp",
NULL, NULL) != 0)
{
return EXIT_FAILURE;
}

if (avformat_find_stream_info(format_ctx, NULL) < 0)
{
return EXIT_FAILURE;
}

// search video stream
for (int i = 0; i < format_ctx->nb_streams; i++)
{
if (format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
video_stream_index = i;
}

AVPacket packet;
av_init_packet(&packet);

// open output file
AVFormatContext *output_ctx = avformat_alloc_context();

AVStream *stream = NULL;
int cnt = 0;

// start reading packets from stream and write them to file
av_read_play(format_ctx); // play RTSP

// Get the codec
AVCodec *codec = NULL;
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec)
{
exit(1);
}

// Add this to allocate the context by codec
codec_ctx = avcodec_alloc_context3(codec);

avcodec_get_context_defaults3(codec_ctx, codec);
avcodec_copy_context(codec_ctx, format_ctx->streams[video_stream_index]->codec);
std::ofstream output_file;

if (avcodec_open2(codec_ctx, codec, NULL) < 0)
exit(1);

img_convert_ctx = sws_getContext(codec_ctx->width, codec_ctx->height,
codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGB24,
SWS_BICUBIC, NULL, NULL, NULL);

int size = avpicture_get_size(AV_PIX_FMT_YUV420P, codec_ctx->width,
codec_ctx->height);
uint8_t *picture_buffer = (uint8_t *)(av_malloc(size));
AVFrame *picture = av_frame_alloc();
AVFrame *picture_rgb = av_frame_alloc();
int size2 = avpicture_get_size(AV_PIX_FMT_RGB24, codec_ctx->width,
codec_ctx->height);
uint8_t *picture_buffer_2 = (uint8_t *)(av_malloc(size2));
avpicture_fill((AVPicture *)picture, picture_buffer, AV_PIX_FMT_YUV420P,
codec_ctx->width, codec_ctx->height);
avpicture_fill((AVPicture *)picture_rgb, picture_buffer_2, AV_PIX_FMT_RGB24,
codec_ctx->width, codec_ctx->height);

while (av_read_frame(format_ctx, &packet) >= 0 && cnt < 1000)
{ // read ~ 1000 frames

std::cout << "1 Frame: " << cnt << std::endl;
if (packet.stream_index == video_stream_index)
{ // packet is video
std::cout << "2 Is Video" << std::endl;
if (stream == NULL)
{ // create stream in file
std::cout << "3 create stream" << std::endl;
stream = avformat_new_stream(output_ctx,
format_ctx->streams[video_stream_index]->codec->codec);
avcodec_copy_context(stream->codec,
format_ctx->streams[video_stream_index]->codec);
stream->sample_aspect_ratio =
format_ctx->streams[video_stream_index]->codec->sample_aspect_ratio;
}
int check = 0;
packet.stream_index = stream->id;
std::cout << "4 decoding" << std::endl;
int result = avcodec_decode_video2(codec_ctx, picture, &check, &packet);
std::cout << "Bytes decoded " << result << " check " << check
<< std::endl;
if (cnt > 100) // cnt < 0)
{
sws_scale(img_convert_ctx, picture->data, picture->linesize, 0,
codec_ctx->height, picture_rgb->data, picture_rgb->linesize);
std::stringstream file_name;
file_name << "test" << cnt << ".ppm";
output_file.open(file_name.str().c_str());
output_file << "P3 " << codec_ctx->width << " " << codec_ctx->height
<< " 255\n";
for (int y = 0; y < codec_ctx->height; y++)
{
for (int x = 0; x < codec_ctx->width * 3; x++)
output_file
<< (int)(picture_rgb->data[0] + y * picture_rgb->linesize[0])[x] << " ";
}
output_file.close();
}
cnt++;
}
av_free_packet(&packet);
av_init_packet(&packet);
}
av_free(picture);
av_free(picture_rgb);
av_free(picture_buffer);
av_free(picture_buffer_2);

av_read_pause(format_ctx);
avio_close(output_ctx->pb);
avformat_free_context(output_ctx);

return (EXIT_SUCCESS);
}

初始化封装上下文

1
2
3
4
5
// Open the initial context variables that are needed
SwsContext *img_convert_ctx;
AVFormatContext *format_ctx = avformat_alloc_context();
AVCodecContext *codec_ctx = NULL;
int video_stream_index;

avformat_alloc_context 初始化封装上下文, 其实这一步不太需要, 因为在后面的 avformat_open_input 方法中也会检查 format_ctx 参数, 如果没有进行初始化的话, 也会将其初始化

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
AVFormatContext *avformat_alloc_context(void)
{
FFFormatContext *const si = av_mallocz(sizeof(*si));
AVFormatContext *s;

if (!si)
return NULL;

//初始化AVStream的操作方法
s = &si->pub;
s->av_class = &av_format_context_class;
s->io_open = io_open_default;
s->io_close = ff_format_io_close_default;
s->io_close2= io_close2_default;

av_opt_set_defaults(s);

//包含音视频数据的包
si->pkt = av_packet_alloc();
//临时数据包,只会被解析或编码程序使用, 不会被av_read_frame与ff_read_packet覆盖
si->parse_pkt = av_packet_alloc();
if (!si->pkt || !si->parse_pkt) {
avformat_free_context(s);
return NULL;
}

si->shortest_end = AV_NOPTS_VALUE;

return s;
}

打开 rtsp 流

1
2
3
4
5
if (avformat_open_input(&format_ctx, "rtsp://134.169.178.187:8554/h264.3gp",
NULL, NULL) != 0)
{
return EXIT_FAILURE;
}

avformat_open_input

avformat_open_input 在不指定 AVInputFormat 的情况下, 会根据 filename 自动查找合适的流处理器与解封器

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
int avformat_open_input(AVFormatContext **ps, const char *filename,
const AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps;
FFFormatContext *si;
AVDictionary *tmp = NULL;
ID3v2ExtraMeta *id3v2_extra_meta = NULL;
int ret = 0;

// 如果事先没有初始化过上下文, 这里会初始化
if (!s && !(s = avformat_alloc_context()))
return AVERROR(ENOMEM);
//统一转换为FFFormatContext指针
si = ffformatcontext(s);
if (!s->av_class) {
av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
return AVERROR(EINVAL);
}
if (fmt)
s->iformat = fmt;

if (options)
av_dict_copy(&tmp, *options, 0);

if (s->pb) // must be before any goto fail
s->flags |= AVFMT_FLAG_CUSTOM_IO;

if ((ret = av_opt_set_dict(s, &tmp)) < 0)
goto fail;

if (!(s->url = av_strdup(filename ? filename : ""))) {
ret = AVERROR(ENOMEM);
goto fail;
}

// init_input 会根据 filename 自动查找合适的解析器
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
s->probe_score = ret;

if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
if (!s->protocol_whitelist) {
ret = AVERROR(ENOMEM);
goto fail;
}
}

if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
if (!s->protocol_blacklist) {
ret = AVERROR(ENOMEM);
goto fail;
}
}

if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist);
ret = AVERROR(EINVAL);
goto fail;
}

avio_skip(s->pb, s->skip_initial_bytes);

/* Check filename in case an image number is expected. */
if (s->iformat->flags & AVFMT_NEEDNUMBER) {
if (!av_filename_number_test(filename)) {
ret = AVERROR(EINVAL);
goto fail;
}
}

s->duration = s->start_time = AV_NOPTS_VALUE;

/* Allocate private data. */
if (s->iformat->priv_data_size > 0) {
if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (s->iformat->priv_class) {
*(const AVClass **) s->priv_data = s->iformat->priv_class;
av_opt_set_defaults(s->priv_data);
if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
goto fail;
}
}

/* e.g. AVFMT_NOFILE formats will not have an AVIOContext */
if (s->pb)
ff_id3v2_read_dict(s->pb, &si->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);

if (s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0) {
if (s->iformat->flags_internal & FF_FMT_INIT_CLEANUP)
goto close;
goto fail;
}

if (!s->metadata) {
s->metadata = si->id3v2_meta;
si->id3v2_meta = NULL;
} else if (si->id3v2_meta) {
av_log(s, AV_LOG_WARNING, "Discarding ID3 tags because more suitable tags were found.\n");
av_dict_free(&si->id3v2_meta);
}

if (id3v2_extra_meta) {
if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
!strcmp(s->iformat->name, "tta") || !strcmp(s->iformat->name, "wav")) {
if ((ret = ff_id3v2_parse_apic(s, id3v2_extra_meta)) < 0)
goto close;
if ((ret = ff_id3v2_parse_chapters(s, id3v2_extra_meta)) < 0)
goto close;
if ((ret = ff_id3v2_parse_priv(s, id3v2_extra_meta)) < 0)
goto close;
} else
av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
}

if ((ret = avformat_queue_attached_pictures(s)) < 0)
goto close;

if (s->pb && !si->data_offset)
si->data_offset = avio_tell(s->pb);

si->raw_packet_buffer_size = 0;

// 设置编解码器
update_stream_avctx(s);

if (options) {
av_dict_free(options);
*options = tmp;
}
*ps = s;
return 0;

close:
if (s->iformat->read_close)
s->iformat->read_close(s);
fail:
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
av_dict_free(&tmp);
if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
avio_closep(&s->pb);
avformat_free_context(s);
*ps = NULL;
return ret;
}

init_input

负责查找解封器

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
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
AVProbeData pd = { filename, NULL, 0 };
int score = AVPROBE_SCORE_RETRY;

if (s->pb) {
s->flags |= AVFMT_FLAG_CUSTOM_IO;
if (!s->iformat)
//如果没有制定 iformat, 但是有拿到数据的话, 则 av_probe_input_buffer2 会调用 av_probe_input_format2(pd, 1, &score) 来查找解封器.
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
else if (s->iformat->flags & AVFMT_NOFILE)
av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
"will be ignored with AVFMT_NOFILE format.\n");
return 0;
}

//如果没有制定 iformat, 也没有制定的 probe 块, 则直接调用 av_probe_input_format2(pd, 0, &score) 来查找解封器.
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
(!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
return score;

// 在这里 io_open 调用的是 avformat_alloc_context 中初始化的缺省 io_open_default,
// 这个方法在 options.c 文件中
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;

if (s->iformat)
return 0;
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
}
av_probe_input_format3

av_probe_input_format2 调用 av_probe_input_format3 进行实际操作

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
const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,
int is_opened, int *score_ret)
{
AVProbeData lpd = *pd;
const AVInputFormat *fmt1 = NULL;
const AVInputFormat *fmt = NULL;
int score, score_max = 0;
void *i = 0;
const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE];
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3;

if (!lpd.buf)
lpd.buf = (unsigned char *) zerobuffer;

//如果有数据则特别处理 ID3V2 的歌词格式
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
int id3len = ff_id3v2_tag_len(lpd.buf);
if (lpd.buf_size > id3len + 16) {
if (lpd.buf_size < 2LL*id3len + 16)
nodat = ID3_ALMOST_GREATER_PROBE;
lpd.buf += id3len;
lpd.buf_size -= id3len;
} else if (id3len >= PROBE_BUF_MAX) {
nodat = ID3_GREATER_MAX_PROBE;
} else
nodat = ID3_GREATER_PROBE;
}

// ffmpeg 在 demuxer_list.c 文件中定义的 demuxer_list[] 常量,
// 这个数组常量包含了本版本源代码中所有的 demuxer.
// 通过 av_demuxer_iterate 方法可以对这些 demuxer 进行查找.
while ((fmt1 = av_demuxer_iterate(&i))) {
if (fmt1->flags & AVFMT_EXPERIMENTAL)
continue;
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
continue;
score = 0;
// 以 rtsp 的 demuxer 为例, 它提供了 read_probe 方法,
// 并在方法中对 rtsp:// 开头的链接进行了确认
if (fmt1->read_probe) {
score = fmt1->read_probe(&lpd);
if (score)
av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
switch (nodat) {
case NO_ID3:
score = FFMAX(score, 1);
break;
case ID3_GREATER_PROBE:
case ID3_ALMOST_GREATER_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
break;
case ID3_GREATER_MAX_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
break;
}
}
// rtsp 的 demuxer 没有提供 extensions
} else if (fmt1->extensions) {
if (av_match_ext(lpd.filename, fmt1->extensions))
score = AVPROBE_SCORE_EXTENSION;
}
// rtsp 的 demuxer 没有提供 mime_type
if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
if (AVPROBE_SCORE_MIME > score) {
av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
score = AVPROBE_SCORE_MIME;
}
}
// 只有分数最高的才返回
if (score > score_max) {
score_max = score;
fmt = fmt1;
} else if (score == score_max)
fmt = NULL;
}
if (nodat == ID3_GREATER_PROBE)
score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
*score_ret = score_max;

return fmt;
}

s->iformat->read_header

读取 rtsp 头信息

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
static int rtsp_read_header(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
int ret;

if (rt->initial_timeout > 0)
rt->rtsp_flags |= RTSP_FLAG_LISTEN;

//如果作为服务器的话, 开始监听端口
if (rt->rtsp_flags & RTSP_FLAG_LISTEN) {
ret = rtsp_listen(s);
if (ret)
return ret;
} else {
// 否则连接 rtsp 服务器
ret = ff_rtsp_connect(s);
if (ret)
return ret;

rt->real_setup_cache = !s->nb_streams ? NULL :
av_calloc(s->nb_streams, 2 * sizeof(*rt->real_setup_cache));
if (!rt->real_setup_cache && s->nb_streams) {
ret = AVERROR(ENOMEM);
goto fail;
}
rt->real_setup = rt->real_setup_cache + s->nb_streams;

// 如果设置的 initial_pause 则连接上后不立刻开始播放
if (rt->initial_pause) {
/* do not start immediately */
} else {
// 开始播放流
ret = rtsp_read_play(s);
if (ret < 0)
goto fail;
}
}

return 0;

fail:
rtsp_read_close(s);
return ret;
}
ff_rtsp_connect
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
int ff_rtsp_connect(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
char proto[128], host[1024], path[1024];
char tcpname[1024], cmd[MAX_URL_SIZE], auth[128];
const char *lower_rtsp_proto = "tcp";
int port, err, tcp_fd;
RTSPMessageHeader reply1, *reply = &reply1;
int lower_transport_mask = 0;
int default_port = RTSP_DEFAULT_PORT;
int https_tunnel = 0;
char real_challenge[64] = "";
struct sockaddr_storage peer;
socklen_t peer_len = sizeof(peer);

if (rt->rtp_port_max < rt->rtp_port_min)
{
av_log(s, AV_LOG_ERROR, "Invalid UDP port range, max port %d less "
"than min port %d\n",
rt->rtp_port_max,
rt->rtp_port_min);
return AVERROR(EINVAL);
}

if (!ff_network_init())
return AVERROR(EIO);

if (s->max_delay < 0) /* Not set by the caller */
s->max_delay = s->iformat ? DEFAULT_REORDERING_DELAY : 0;

//调用 ff_network_init 初始化网络
// 设置缺省使用rtsp传输协议,
rt->control_transport = RTSP_MODE_PLAIN;
// 如果设置了使用 http 或者 https 则设置使用 http tunnel 协议
if (rt->lower_transport_mask & ((1 << RTSP_LOWER_TRANSPORT_HTTP) |
(1 << RTSP_LOWER_TRANSPORT_HTTPS)))
{
https_tunnel = !!(rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_HTTPS));
rt->lower_transport_mask = 1 << RTSP_LOWER_TRANSPORT_TCP;
rt->control_transport = RTSP_MODE_TUNNEL;
}
/* Only pass through valid flags from here */
rt->lower_transport_mask &= (1 << RTSP_LOWER_TRANSPORT_NB) - 1;

redirect:
memset(&reply1, 0, sizeof(reply1));
/* extract hostname and port */
av_url_split(proto, sizeof(proto), auth, sizeof(auth),
host, sizeof(host), &port, path, sizeof(path), s->url);
// 在 rtsps 协议情况下会使用322端口, 否则使用缺省的554端口
if (!strcmp(proto, "rtsps"))
{
lower_rtsp_proto = "tls";
default_port = RTSPS_DEFAULT_PORT;
rt->lower_transport_mask = 1 << RTSP_LOWER_TRANSPORT_TCP;
}
else if (!strcmp(proto, "satip"))
{
av_strlcpy(proto, "rtsp", sizeof(proto));
rt->server_type = RTSP_SERVER_SATIP;
}

if (*auth)
{
av_strlcpy(rt->auth, auth, sizeof(rt->auth));
}
if (port < 0)
port = default_port;

lower_transport_mask = rt->lower_transport_mask;

if (!lower_transport_mask)
lower_transport_mask = (1 << RTSP_LOWER_TRANSPORT_NB) - 1;
// 在 RTSP_MODE_TUNNEL 情况下是不支持上传流数据的
if (s->oformat)
{
/* Only UDP or TCP - UDP multicast isn't supported. */
lower_transport_mask &= (1 << RTSP_LOWER_TRANSPORT_UDP) |
(1 << RTSP_LOWER_TRANSPORT_TCP);
if (!lower_transport_mask || rt->control_transport == RTSP_MODE_TUNNEL)
{
av_log(s, AV_LOG_ERROR, "Unsupported lower transport method, "
"only UDP and TCP are supported for output.\n");
err = AVERROR(EINVAL);
goto fail;
}
}

/* Construct the URI used in request; this is similar to s->url,
* but with authentication credentials removed and RTSP specific options
* stripped out. */
// 生成 rtsp 控制地址
ff_url_join(rt->control_uri, sizeof(rt->control_uri), proto, NULL,
host, port, "%s", path);

if (rt->control_transport == RTSP_MODE_TUNNEL)
{
/* set up initial handshake for tunneling */
char httpname[1024];
char sessioncookie[17];
char headers[1024];
AVDictionary *options = NULL;

av_dict_set_int(&options, "timeout", rt->stimeout, 0);

ff_url_join(httpname, sizeof(httpname), https_tunnel ? "https" : "http", auth, host, port, "%s", path);
snprintf(sessioncookie, sizeof(sessioncookie), "%08x%08x",
av_get_random_seed(), av_get_random_seed());

/* GET requests */
if (ffurl_alloc(&rt->rtsp_hd, httpname, AVIO_FLAG_READ,
&s->interrupt_callback) < 0)
{
err = AVERROR(EIO);
goto fail;
}

/* generate GET headers */
snprintf(headers, sizeof(headers),
"x-sessioncookie: %s\r\n"
"Accept: application/x-rtsp-tunnelled\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n",
sessioncookie);
av_opt_set(rt->rtsp_hd->priv_data, "headers", headers, 0);

if (!rt->rtsp_hd->protocol_whitelist && s->protocol_whitelist)
{
rt->rtsp_hd->protocol_whitelist = av_strdup(s->protocol_whitelist);
if (!rt->rtsp_hd->protocol_whitelist)
{
err = AVERROR(ENOMEM);
goto fail;
}
}

/* complete the connection */
if (ffurl_connect(rt->rtsp_hd, &options))
{
av_dict_free(&options);
err = AVERROR(EIO);
goto fail;
}

/* POST requests */
if (ffurl_alloc(&rt->rtsp_hd_out, httpname, AVIO_FLAG_WRITE,
&s->interrupt_callback) < 0)
{
err = AVERROR(EIO);
goto fail;
}

/* generate POST headers */
snprintf(headers, sizeof(headers),
"x-sessioncookie: %s\r\n"
"Content-Type: application/x-rtsp-tunnelled\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n"
"Content-Length: 32767\r\n"
"Expires: Sun, 9 Jan 1972 00:00:00 GMT\r\n",
sessioncookie);
av_opt_set(rt->rtsp_hd_out->priv_data, "headers", headers, 0);
av_opt_set(rt->rtsp_hd_out->priv_data, "chunked_post", "0", 0);
av_opt_set(rt->rtsp_hd_out->priv_data, "send_expect_100", "0", 0);

/* Initialize the authentication state for the POST session. The HTTP
* protocol implementation doesn't properly handle multi-pass
* authentication for POST requests, since it would require one of
* the following:
* - implementing Expect: 100-continue, which many HTTP servers
* don't support anyway, even less the RTSP servers that do HTTP
* tunneling
* - sending the whole POST data until getting a 401 reply specifying
* what authentication method to use, then resending all that data
* - waiting for potential 401 replies directly after sending the
* POST header (waiting for some unspecified time)
* Therefore, we copy the full auth state, which works for both basic
* and digest. (For digest, we would have to synchronize the nonce
* count variable between the two sessions, if we'd do more requests
* with the original session, though.)
*/
ff_http_init_auth_state(rt->rtsp_hd_out, rt->rtsp_hd);

/* complete the connection */
if (ffurl_connect(rt->rtsp_hd_out, &options))
{
av_dict_free(&options);
err = AVERROR(EIO);
goto fail;
}
av_dict_free(&options);
}
else //使用缺省的 rtsp 协议
{
int ret;
/* open the tcp connection */
ff_url_join(tcpname, sizeof(tcpname), lower_rtsp_proto, NULL,
host, port,
"?timeout=%" PRId64, rt->stimeout);
// 打开 rtsp 流地址, 在 ffurl_open_whitelist 中会
// 通过 ffurl_alloc 函数生成 rtsp_hd 这个 URLContext 对象
if ((ret = ffurl_open_whitelist(&rt->rtsp_hd, tcpname, AVIO_FLAG_READ_WRITE,
&s->interrupt_callback, NULL, s->protocol_whitelist, s->protocol_blacklist, NULL)) < 0)
{
err = ret;
goto fail;
}
rt->rtsp_hd_out = rt->rtsp_hd;
}
rt->seq = 0;

tcp_fd = ffurl_get_file_handle(rt->rtsp_hd);
if (tcp_fd < 0)
{
err = tcp_fd;
goto fail;
}
if (!getpeername(tcp_fd, (struct sockaddr *)&peer, &peer_len))
{
getnameinfo((struct sockaddr *)&peer, peer_len, host, sizeof(host),
NULL, 0, NI_NUMERICHOST);
}

/* request options supported by the server; this also detects server
* type */
if (rt->server_type != RTSP_SERVER_SATIP)
rt->server_type = RTSP_SERVER_RTP;

// 向服务器发出 OPTIONS 命令, 直到返回错误或者正确的信息,
// OPTIONS 命令会告诉客户端, 服务端的能力, 并且也表明服务端正确响应了客户端,
// 可以开始进一步的通讯了
for (;;)
{
cmd[0] = 0;
if (rt->server_type == RTSP_SERVER_REAL)
av_strlcat(cmd,
/*
* The following entries are required for proper
* streaming from a Realmedia server. They are
* interdependent in some way although we currently
* don't quite understand how. Values were copied
* from mplayer SVN r23589.
* ClientChallenge is a 16-byte ID in hex
* CompanyID is a 16-byte ID in base64
*/
"ClientChallenge: 9e26d33f2984236010ef6253fb1887f7\r\n"
"PlayerStarttime: [28/03/2003:22:50:23 00:00]\r\n"
"CompanyID: KnKV4M4I/B2FjJ1TToLycw==\r\n"
"GUID: 00000000-0000-0000-0000-000000000000\r\n",
sizeof(cmd));
ff_rtsp_send_cmd(s, "OPTIONS", rt->control_uri, cmd, reply, NULL);
if (reply->status_code != RTSP_STATUS_OK)
{
err = ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
goto fail;
}

/* detect server type if not standard-compliant RTP */
if (rt->server_type != RTSP_SERVER_REAL && reply->real_challenge[0])
{
rt->server_type = RTSP_SERVER_REAL;
continue;
}
else if (!av_strncasecmp(reply->server, "WMServer/", 9))
{
rt->server_type = RTSP_SERVER_WMS;
}
else if (rt->server_type == RTSP_SERVER_REAL)
strcpy(real_challenge, reply->real_challenge);
break;
}

#if CONFIG_RTSP_DEMUXER
if (s->iformat)
{
if (rt->server_type == RTSP_SERVER_SATIP)
err = init_satip_stream(s);
else
// 设置解析流
err = ff_rtsp_setup_input_streams(s, reply);
}
else
#endif
if (CONFIG_RTSP_MUXER)
err = ff_rtsp_setup_output_streams(s, host);
else
av_assert0(0);
if (err)
goto fail;

do
{
int lower_transport = ff_log2_tab[lower_transport_mask &
~(lower_transport_mask - 1)];

if ((lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) && (rt->rtsp_flags & RTSP_FLAG_PREFER_TCP))
lower_transport = RTSP_LOWER_TRANSPORT_TCP;

err = ff_rtsp_make_setup_request(s, host, port, lower_transport,
rt->server_type == RTSP_SERVER_REAL ? real_challenge : NULL);
if (err < 0)
goto fail;
lower_transport_mask &= ~(1 << lower_transport);
if (lower_transport_mask == 0 && err == 1)
{
err = AVERROR(EPROTONOSUPPORT);
goto fail;
}
} while (err);

rt->lower_transport_mask = lower_transport_mask;
av_strlcpy(rt->real_challenge, real_challenge, sizeof(rt->real_challenge));
rt->state = RTSP_STATE_IDLE;
rt->seek_timestamp = 0; /* default is to start stream at position zero */
return 0;
fail:
ff_rtsp_close_streams(s);
ff_rtsp_close_connections(s);
if (reply->status_code >= 300 && reply->status_code < 400 && s->iformat)
{
char *new_url = av_strdup(reply->location);
if (!new_url)
{
err = AVERROR(ENOMEM);
goto fail2;
}
ff_format_set_url(s, new_url);
rt->session_id[0] = '\0';
av_log(s, AV_LOG_INFO, "Status %d: Redirecting to %s\n",
reply->status_code,
s->url);
goto redirect;
}
fail2:
ff_network_close();
return err;
}

ffurl_open_whitelist
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
int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options,
const char *whitelist, const char* blacklist,
URLContext *parent)
{
AVDictionary *tmp_opts = NULL;
AVDictionaryEntry *e;
// 通过 avio.c 文件中的 url_find_protocol 函数, 使用
// ffurl_get_protocols 在 protocol_list.c 文件定义的常量 url_protocols 中找到
// ff_rtp_protocol 这个 rtp 传输协议解析器
// 然后把传输协议放入 puc->prot
int ret = ffurl_alloc(puc, filename, flags, int_cb);
if (ret < 0)
return ret;
if (parent) {
ret = av_opt_copy(*puc, parent);
if (ret < 0)
goto fail;
}
if (options &&
(ret = av_opt_set_dict(*puc, options)) < 0)
goto fail;
if (options && (*puc)->prot->priv_data_class &&
(ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
goto fail;

if (!options)
options = &tmp_opts;

av_assert0(!whitelist ||
!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
!strcmp(whitelist, e->value));
av_assert0(!blacklist ||
!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
!strcmp(blacklist, e->value));

if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0)
goto fail;

if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0)
goto fail;

if ((ret = av_opt_set_dict(*puc, options)) < 0)
goto fail;

// 使用得到的 rtp 传输协议连接 rtsp 服务
ret = ffurl_connect(*puc, options);

if (!ret)
return 0;
fail:
ffurl_closep(puc);
return ret;
}

url_protocols

protocol_list.c 文件定义的常量 url_protocols 中找到 ff_rtp_protocol 这个 rtp 传输协议解析器.

1
2
3
4
5
6
7
8
9
10
11
12
const URLProtocol ff_rtp_protocol = {
.name = "rtp",
.url_open = rtp_open,
.url_read = rtp_read,
.url_write = rtp_write,
.url_close = rtp_close,
.url_get_file_handle = rtp_get_file_handle,
.url_get_multi_file_handle = rtp_get_multi_file_handle,
.priv_data_size = sizeof(RTPContext),
.flags = URL_PROTOCOL_FLAG_NETWORK,
.priv_data_class = &rtp_class,
};
ffurl_connect
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
int ffurl_connect(URLContext *uc, AVDictionary **options)
{
int err;
AVDictionary *tmp_opts = NULL;
AVDictionaryEntry *e;

if (!options)
options = &tmp_opts;

// Check that URLContext was initialized correctly and lists are matching if set
av_assert0(!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
(uc->protocol_whitelist && !strcmp(uc->protocol_whitelist, e->value)));
av_assert0(!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
(uc->protocol_blacklist && !strcmp(uc->protocol_blacklist, e->value)));

if (uc->protocol_whitelist && av_match_list(uc->prot->name, uc->protocol_whitelist, ',') <= 0) {
av_log(uc, AV_LOG_ERROR, "Protocol '%s' not on whitelist '%s'!\n", uc->prot->name, uc->protocol_whitelist);
return AVERROR(EINVAL);
}

if (uc->protocol_blacklist && av_match_list(uc->prot->name, uc->protocol_blacklist, ',') > 0) {
av_log(uc, AV_LOG_ERROR, "Protocol '%s' on blacklist '%s'!\n", uc->prot->name, uc->protocol_blacklist);
return AVERROR(EINVAL);
}

if (!uc->protocol_whitelist && uc->prot->default_whitelist) {
av_log(uc, AV_LOG_DEBUG, "Setting default whitelist '%s'\n", uc->prot->default_whitelist);
uc->protocol_whitelist = av_strdup(uc->prot->default_whitelist);
if (!uc->protocol_whitelist) {
return AVERROR(ENOMEM);
}
} else if (!uc->protocol_whitelist)
av_log(uc, AV_LOG_DEBUG, "No default whitelist set\n"); // This should be an error once all declare a default whitelist

if ((err = av_dict_set(options, "protocol_whitelist", uc->protocol_whitelist, 0)) < 0)
return err;
if ((err = av_dict_set(options, "protocol_blacklist", uc->protocol_blacklist, 0)) < 0)
return err;

// 使用 rtp 协议中的 rtp_open 方法连接s
err =
uc->prot->url_open2 ? uc->prot->url_open2(uc,
uc->filename,
uc->flags,
options) :
uc->prot->url_open(uc, uc->filename, uc->flags);

av_dict_set(options, "protocol_whitelist", NULL, 0);
av_dict_set(options, "protocol_blacklist", NULL, 0);

if (err)
return err;
uc->is_connected = 1;
/* We must be careful here as ffurl_seek() could be slow,
* for example for http */
if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
uc->is_streamed = 1;
return 0;
}

rtp_open
1
2
3
4
5
6
7
static int rtp_open(URLContext *h, const char *uri, int flags)
{
// rtp的实际连接是个udp连接, 在这个函数中根据rtp连接字符串重新构建udp连接字符串
// 再通过 ffurl_open_whitelist 函数去打开 udp 连接
// 这里打开的 upd 连接有两个, 一个是 rtp 的数据传输连接, 一个是 rtcp 的协议控制连接
// fec 前向错误矫正, 如果有这的话, 还要多一个连接
}
ff_rtsp_setup_input_stream

发送 DESCRIBE 命令, 得到媒体流的格式数据, 通过 ff_sdp_parse 解析返回的结果后对解封装器进行设置, 其中

  1. 如果不是MP2T的封装格式, 通过 ff_rtp_handler_find_by_id 从 rtpdesc.c 文件中定义的 rtp_dynamic_protocol_handler_list 中找到解码器
  2. 如果是私有协议类型, ff_rtp_get_codec_info 函数得到缺省支持的媒体类型, 然后再从 ff_rtp_handler_find_by_id 中得到对应的解码器
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
int ff_rtsp_setup_input_streams(AVFormatContext *s, RTSPMessageHeader *reply)
{
RTSPState *rt = s->priv_data;
char cmd[MAX_URL_SIZE];
unsigned char *content = NULL;
int ret;

/* describe the stream */
snprintf(cmd, sizeof(cmd),
"Accept: application/sdp\r\n");
if (rt->server_type == RTSP_SERVER_REAL) {
/**
* The Require: attribute is needed for proper streaming from
* Realmedia servers.
*/
av_strlcat(cmd,
"Require: com.real.retain-entity-for-setup\r\n",
sizeof(cmd));
}
ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, &content);
if (reply->status_code != RTSP_STATUS_OK) {
av_freep(&content);
return ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
}
if (!content)
return AVERROR_INVALIDDATA;

av_log(s, AV_LOG_VERBOSE, "SDP:\n%s\n", content);
/* now we got the SDP description, we parse it */
ret = ff_sdp_parse(s, (const char *)content);
av_freep(&content);
if (ret < 0)
return ret;

return 0;
}
ff_rtsp_make_setup_request

发送 SETUP 命令给 rtsp 服务器

rtsp_read_play
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
static int rtsp_read_play(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
RTSPMessageHeader reply1, *reply = &reply1;
int i;
char cmd[MAX_URL_SIZE];

av_log(s, AV_LOG_DEBUG, "hello state=%d\n", rt->state);
rt->nb_byes = 0;

if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP) {
for (i = 0; i < rt->nb_rtsp_streams; i++) {
RTSPStream *rtsp_st = rt->rtsp_streams[i];
/* Try to initialize the connection state in a
* potential NAT router by sending dummy packets.
* RTP/RTCP dummy packets are used for RDT, too.
*/
if (rtsp_st->rtp_handle &&
!(rt->server_type == RTSP_SERVER_WMS && i > 1))
ff_rtp_send_punch_packets(rtsp_st->rtp_handle);
}
}
if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subscription)) {
if (rt->transport == RTSP_TRANSPORT_RTP) {
for (i = 0; i < rt->nb_rtsp_streams; i++) {
RTSPStream *rtsp_st = rt->rtsp_streams[i];
RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
if (!rtpctx)
continue;
ff_rtp_reset_packet_queue(rtpctx);
rtpctx->last_rtcp_ntp_time = AV_NOPTS_VALUE;
rtpctx->first_rtcp_ntp_time = AV_NOPTS_VALUE;
rtpctx->base_timestamp = 0;
rtpctx->timestamp = 0;
rtpctx->unwrapped_timestamp = 0;
rtpctx->rtcp_ts_offset = 0;
}
}
if (rt->state == RTSP_STATE_PAUSED) {
cmd[0] = 0;
} else {
snprintf(cmd, sizeof(cmd),
"Range: npt=%"PRId64".%03"PRId64"-\r\n",
rt->seek_timestamp / AV_TIME_BASE,
rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
}
ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
if (reply->status_code != RTSP_STATUS_OK) {
return ff_rtsp_averror(reply->status_code, -1);
}
if (rt->transport == RTSP_TRANSPORT_RTP &&
reply->range_start != AV_NOPTS_VALUE) {
for (i = 0; i < rt->nb_rtsp_streams; i++) {
RTSPStream *rtsp_st = rt->rtsp_streams[i];
RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
AVStream *st = NULL;
if (!rtpctx || rtsp_st->stream_index < 0)
continue;

st = s->streams[rtsp_st->stream_index];
rtpctx->range_start_offset =
av_rescale_q(reply->range_start, AV_TIME_BASE_Q,
st->time_base);
}
}
}
rt->state = RTSP_STATE_STREAMING;
return 0;
}

查询流信息

1
2
3
4
if (avformat_find_stream_info(format_ctx, NULL) < 0)
{
return EXIT_FAILURE;
}

avformat_find_stream_info

背包

sixmoondesign的swift X和minimalism

  • 优势

    • 肩带仿照越野跑背包肩带,方便饮用水和路粮
  • 劣势

    • 重量对比hmg2400没有明显的降低
    • 侧面网兜的结实性有待验证

waymark lite

  • 优势
    • 可定制大的侧袋
    • 前兜网可定制为网兜
  • 劣势
    • 无背负,需要验证整体装备的重量和转包能力

HMG 2400

  • 优势
    • 我有
  • 劣势
    • 可能容量不够

帐篷

tarptent notch li

  • 优势
    • 重量在考虑范围内
    • 成型设计,只需要六根地丁
  • 劣势
    • 太贵
    • 0.5oz外皮可能会太脆弱

酱铺1.5人帐篷

  • 优势
    • 重量在考虑范围内
    • 价格相对合理
  • 劣势
    • 还没有看到具体设计细节,不好做出评价

零碎

水具

  • 可用农夫山泉的高级矿泉水瓶代替鸭嘴兽水袋和钛水壶

打包

  • 不把食物都装在一个大袋子里,而且根据每天的量分开为多个袋子,应该可以减小对空间的浪费

  • 把neoair xlite折叠后靠背负放置

  • 带更小的帐篷,不带巨大的金字塔,这就意味着需要升级帐篷

结论

2020.10.20

先升级帐篷,之后再整体打算

Apple Watch 和 WorkOutDoors

之前主要使用手机配合Garmin 935进行户外导航。手机上下载离线地图和轨迹,Garmin 935上本身也有提供轨迹的功能,可以见Garmin-forerunner-935-添加导航路径一文。手表导航确实有用,贡嘎那次就因为手表导航中偏离航线报警,从而避免了一次无意义的过河。这次手表换成了Apple Watch Series 5,导航就成了问题。手表导航的好处在于随手就可以看到,而手机还需要停下来认真看。经过查找,参考Apple Watch Hiking & Backpacking Review,里面推荐了WorkOutDoors这个软件。

WorkOutDoors这个软件本身可以认为是一个功能比较完善的户外导航软件。

  1. 它可以定制表盘,显示你需要的户外徒步中显示的数据

    IMG_3CDDD0F734F2-1

  2. 它可以导入gpx的航迹

    IMG_94C38119F7FA-1

  3. 它可以下载离线地图,而且这个地图应该是优化过的,体积非常小

    IMG_2F20EB3D7705-1

在手机上导入的航迹、下载的离线地图都可以直接发送到Apple Watch中,得益于S5处理器的强大,运行和操作非常流畅。

IMG_2390

在这次亚丁转山的过程中,手机的导航基本没有使用,一直是通过手表上的导航,手机的电量得到了最大化的保存。虽然WorkOutDoors在Apple Watch上大约只能使用八到十个小时,但是对于一般情况下的徒步是足够了。只是这个软件有个bug,最后在使用完时,需要手动关闭并保存数据,如果中途手表没有电了,那么你这几个小时的数据也就没有了,我就是因为这个原因,中途有一天的数据没有了。

可以看到四号的数据不见了

IMG_7D4358217882-1

Apple Watch本来就是要一天一充的,在使用WorkOutDoors时就需要每天早上充满电,才可以完好的在徒步中为你服务,并在结束时完好地保存数据。

lii gear 凯夫拉挎包

939952503928844820200306225129312438

lii gear MUSSTTE BAG 凯夫拉版本逼格非常的高。但这次户外中发现,这高逼格的东西其实并不合适自己。这个挎包本身偏机能风,在城里使用是挺好的,但是对于我这种走垃圾轻量化路线的人来说整体偏重。而且更严重的是,它的肩带调节需要额外的一根“winner winner chicken dinner”的带子才会比较容易的进行,这就莫名的添加了重量,如果不带这个根带子,那么调节长度就变得非常麻烦。

酱铺防潮垫

垃圾佬的背影

背影

上图中那个黑粗的就是酱铺的防潮垫。这个垫子是真的轻,整体只有230克。在使用的过程中这么轻的垫子耐操度还可以,过杜鹃林时各种剐蹭都没有破,而且保暖性也相当的不错。

前面提到过杜鹃林时各种剐蹭,这就是我不再使用它的原因。个人比较喜欢整体清爽而紧凑的打包,这种包外挂着一个巨大的防潮垫的做法真是我不喜,并且剐蹭对于徒步的流畅性来说也是挺有影响。同时这么高的包也不好使用雨伞,而对于除非非常必要不喜欢冲锋衣和雨衣的我来说,这点也是不能忍受。于是在徒步结束后,这个防潮垫就被我放弃在稻城的旅馆中了。

之后还是用回NeoAir Xlite,有可能考虑添加一个迪卡侬的折叠防潮垫作为屁垫使用,只是这样子重量不免增加不少。

酱铺摩尔背架

一直以来,都是使用酱铺背架的完全体,重型腰带、背部弓板、摩尔包、大河防水袋。这套用了几年,毕竟用高上限的背包背轻的东西会更舒服。但这是真的吗?这几年我五天左右的徒步最重其实都没有超过12公斤,而这其中背包就要占用1.3到1.5公斤左右。虽然1:10已经是不错的重量比,但自我感觉已经到了一个可以更轻的时间点了。

太阳能板

高原的太阳再次证明了自己的威力,这次天气不太好,太阳总在厚厚的云层中偶尔才会露出来,而到了营地基本就是下雨。

IMG_2244

就算是这样子,太阳能板依然能每天把小米一万毫安的充电宝续命一格电,五天下来每天冲手机充手表充头灯,最后还有一格电足够从景区续命到稻城住下,以后去西部太阳能板看来是必带的了。

不过这次有个问题就是线损的存在。本次使用的是一根一米长的紫米type-c充电线,通过type-c转MicroUSB、type-c转Lightning,USB转type-c的转接器对接各种需要充电的电器。说来十分方便,但是用手按在转接头上就知道了,发热很大,说明有很大一部分能量浪费掉了。所以下次一方面要把一米的线换成更短的线,还要把转接头换成专门的线来提高效率。

酒精炉

这次除了在贡嘎扎则外,全程使用酒精,在只烧水泡山屋、尾西、咖啡、苏伯汤的情况下,全程用了大约600ml的酒精,就标准来说用量挺大,主要是这次使用的小姜手做的酒精炉。这个炉子的挺好,燃烧的不会过快,效率挺高的。但是缺点也很大,就是不好掌握酒精的用量,而且剩余的酒精也没有办法回收。这次就是在加一次不够的情况下,再加一次酒精最后一定会过多,但是没有办法回收这部分酒精,只能在炉体冷却的过程中自然挥发掉,挺浪费的。

IMG_E35F8C47D2FD-1

在徒步前的准备阶段,从重量考虑一般会把要吃的食物从包装袋中取出来放入更轻的密封袋中。但在2018年徒步熬太的过程中,我把山之厨保留了一包,然后全程使用这包的包装袋当作锅和碗。泡山之厨、泡尾西、泡苏伯汤,完全没有问题,而且还不需要洗碗。这次没有这么做,明显麻烦了很多。锅就一个,泡了山屋就不能烧水,要吃完饭喝苏伯汤的话还要在起火,而且吃完了还要洗,所以以后还是要留一个包装袋做碗,而且保温瓶还是带有单独盖子的,梦重力轻是很轻,但是保温明显不行。

食物

FullSizeRender

  • 冻干鸡胸肉不好吃,太干太柴了,下次不带了
  • 网易严选牛肉不错,放在家里也是过期,下次可以考虑
  • 在香格里拉镇买的鱼皮、鱼干实在太油腻了,特别是鱼皮,吃了上火,以后要坚决避免买这种上火的东西当路粮。
  • 下次可以试试带家里晒干的小鱼干
  • 都是腰果太单调,下次换一下沃尔玛的综合坚果试试
  • 柠檬味士力架天冷吃很不错,柠檬味在城里吃非常的香精,但是在户外吃还挺清爽的
  • 黑芝麻糊作为中午路粮很好,每天路上都盼望着那个吃黑芝麻糊的时刻
  • 雀巢的冻干咖啡味道很好,能比较好的还原咖啡香味和口味,下次可以继续带
  • 苏伯汤配上脱水蔬菜能很好的补充纤维的不足,也能满足心理的需要
  • 下次带点善存等复合维生素片,这次没带上火了,而且手上的伤不容易好
  • 下次记得携带宝矿力活着其他运动饮料冲泡剂

数量问题

  • 这次带了24块士力架,最后剩余11块,平均一天两块,但实际上在强度比较大的时候,一天会用到3块至4块,以后可以照平均一天4块进行携带,同时减少其他零食的数量。

  • 这次带了500g的腰果,路途中和营地都有吃,最后剩余200g左右,说明500g腰果能支撑8天左右的路程。

  • 冻干咖啡可以考虑照一天两条的数量进行携带,以支持中午13点至15点间的体力低潮

  • 可以考虑携带猪肉圃

  • 24根士力架 剩余11条

  • 5包网易牛肉干 吃完

  • 500克腰果 吃完

  • 5份黑芝麻糊 吃完

  • 50克干鸡胸肉 剩余40克

  • 50克脱水蔬菜 剩余15克

  • 10包苏伯汤 吃完

  • 5份速溶咖啡 吃完

  • 2份姜糖 吃完

  • 5份山屋 吃完

  • 5份尾西 吃完

  • 一份意大利面 吃完

随着大量公司将办公会议、产品发布等改为网上进行后, 视频通信的安全性越来越成为受关注的重点。声网平台在 2.1.0 版本之后,通过使用 AccessToken 认证统一了视频通话RTC、录制、消息通讯RTM等各 SDK 的安全认证形式,相较于原先的 DynmicKey 更加方便于用户使用。

.NET Core 是微软的跨平台开发框架,可运行在 Windows、Linux、macOS 等操作系统之上,通过命令行工具就可以方便的创建、编译、运行,并可搭配 Docker 容器使用,方便嵌入微服务架构中。

本文将基于 .NET Core 3.1 版本说明如何建立一个 Agora RTC Token 服务,同样这个服务也可以用于录制和 RTM SDK中。

预备知识

  • 本文默认读者了解基本的 C# 编程知识,如果有需要可以访问C#文档 进行了解。
  • 本文需要 ASP.NET Core 及相关的 WebAPI 知识,如果有需要可以访问ASP.NET 文档进行了解。
  • 本文会有一点点 Git 相关的使用,但不是必要的。

本文所需工具

  • .NET Core SDK - 包括 .NET Core 运行时、开发包及命令行工具。
  • Visual Studio Code - 微软推出的跨平台开发工具,你也可以使用自己喜欢或习惯的开发工具。
  • .NET Core开发环境配置 - 如果你刚开始使用 Visual Studio Code,推荐阅读这个链接中的安装配置。
  • Git - 本文会使用到 Git 但不是必要条件,在相应章节会进行说明。

项目创建

  1. 打开终端,进入你平时开发目录
  2. 运行以下命令
1
2
dotnet new webapi -o AgoraTokenServer
code -r AgoraTokenServer
  1. 如果你正确的安装了 Visual Studio Code 的话,这时系统应该会打开 Visual Studio Code 程序并将 AgoraTokenServer 这个项目显示在左侧,如下图所示:

    为了方便起见,以下 Visual Studio Code 将简称为 vscode。此时整个项目的目录结构应该如下图所示:

我们将 WeatherForecast.cs 与 Controllers/WeatherForecastController.cs 删除,稍后我们将建立起自己的服务。

开发

引入工具代码

Agora 在其AgoraIO in GitHub中提供了 AccessToken 的 C# 实现,我们可以直接使用它。

  1. 进入AgoraIO in GitHub,点击页面上那个绿色的Code按钮

  1. 如果你会 Git 那么可以直接在其他目录中,注意不要直接在上一章节建立的 AgoraTokenServer 项目目录中,把项目克隆下来。
1
git clone https://github.com/AgoraIO/Tools.git

​ 如果你不会 Git ,可以直接点击 Download ZIP 将其下载下来并解压缩。

  1. 进入刚刚 Git 克隆或者下载解压缩后的目录

    1
    cd Tools/DynamicKey/AgoraDynamicKey/csharp/src/AgoraIO

    将其中的 Common、Extensions、Media、Utils 四个目录直接拷贝至你创建的 AgoraTokenServer 目录下,之后你的 AgoraTokenServer 目录结构应该是如下图这样子的:

解决依赖

你会发现上图中 Media/AccessToken.cs 是红色的,那是因为这个项目依赖于Crc32.NET这个包,如果你正确的安装了 .NET Core 的运行时和命令行工具的话 我们直接使用命令行将其安装就可以了。

进入 AgoraTokenServer 项目的根目录下,运行如下命令:

1
dotnet add package Crc32.NET

这样子我们唯一一个外部依赖包就解决了。

设置 AppID 与 AppCertificate

  1. 在通常环境中 AppCertificate 应当保存在安全性较高的服务端,不宜通过客户端请求进行传输,在 .NET Core 中这种设置通常可以保存在 appsetting.json 中。下面 appsetting.json 代码中的 AppID 和 AppCertificate 为示例,请在使用中替换为自己使用的对应 AppID 和 AppCertificate。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "AppSettings": {
    "AppID": "970CA35de60c44645bbae8a215061b33",
    "AppCertificate": "5CFd2fd1755d40ecb72977518be15d3b"
    },
    "Logging": {
    "LogLevel": {
    "Default": "Information",
    "Microsoft": "Warning",
    "Microsoft.Hosting.Lifetime": "Information"
    }
    },
    "AllowedHosts": "*"
    }
  2. 建立配置类

    在 Utils 目录下创建一个名为 AppSettings.cs 的文件,文件内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    namespace AgoraTokenServer.Utils
    {
    public class AppSettings
    {
    public string AppID { get; set; }
    public string AppCertificate { get; set; }
    }
    }
  3. 注入配置类

    ASP.NET Core 使用依赖注入来解决整个程序的依赖问题,通过这个机制我们可以很方便的把上面定义的配置注入进去。依赖注入需要在 Startup.cs 文件中添加自定义的配置类,添加后 Startup.cs 文件内容如下:

    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
    using AgoraTokenServer.Utils;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;

    namespace AgoraTokenServer
    {
    public class Startup
    {
    public Startup(IConfiguration configuration)
    {
    Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
    services.AddCors(); //添加跨域请求
    services.AddControllers();
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings")); //添加程序配置
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    if (env.IsDevelopment())
    {
    app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseCors(x => x
    .AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader());

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
    endpoints.MapControllers();
    });
    }
    }
    }

创建 Model

我们先定义两个对象来描述接受的内容和返回的结果。首先建立一个名为 Models 的目录,再在目录下创建两个文件。

  1. 请求对象文件

    Path: /Models/AuthenticateRequest.cs

    在 Models 目录下创建 AuthenticateRequest.cs 文件,文件内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    using System.ComponentModel.DataAnnotations;

    namespace AgoraTokenServer.Models
    {
    public class AuthenticateRequest
    {
    [Required]
    public string channel { get; set; }
    [Required]
    public dynamic uid { get; set; }

    public uint expiredTs { get; set; } = 0;

    public int role { get; set; } = 1;
    }
    }

    因为 Agora 的用户标识有两种类型,一种是 uint 型,一种是 string 型的,所以这里直接使用 dynamic 类型来同时兼容两种类型。

  2. 回应对象

    Path: /Models/AuthenticateResponse.cs

    在 Models 目录下创建 AuthenticateResponse.cs 文件,文件内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace AgoraTokenServer.Models
    {
    public class AuthenticateResponse
    {
    public string channel { get; set; }
    public dynamic uid { get; set; }
    public string token { get; set; }
    }
    }
  3. 现在项目的结构如下图:

创建服务

  1. 现在我们创建一个控制器来提供服务,首先在 AgoraTokenServer 项目的 Controllers 目录下建立一个名为 AccessTokenController.cs 的文件。

    Path: /Controllers/AccessTokenController.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    using AgoraTokenServer.Models;
    using Microsoft.AspNetCore.Mvc;

    namespace AgoraTokenServer.Contollers
    {
    [ApiController]
    [Route("[controller]")]
    public class AccessTokenController : ControllerBase
    {
    }
    }
  2. 添加程序配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    using AgoraTokenServer.Utils;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Options;

    namespace AgoraTokenServer.Contollers
    {
    [ApiController]
    [Route("[controller]")]
    public class AccessTokenController : ControllerBase
    {
    private readonly AppSettings appSettings;

    public AccessTokenController(IOptions<AppSettings> appSettings)
    {
    this.appSettings = appSettings.Value;
    }

    }
    }
  3. 添加请求处理部分

    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
    using System.Net;
    using System.Text.Json;
    using AgoraIO.Media;
    using AgoraTokenServer.Models;
    using AgoraTokenServer.Utils;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Options;

    namespace AgoraTokenServer.Contollers
    {
    [ApiController]
    [Route("[controller]")]
    public class AccessTokenController : ControllerBase
    {
    private readonly AppSettings appSettings;

    public AccessTokenController(IOptions<AppSettings> appSettings)
    {
    this.appSettings = appSettings.Value;
    }

    [HttpPost]
    public ActionResult<AuthenticateResponse> index(AuthenticateRequest request)
    {
    if (string.IsNullOrEmpty(appSettings.AppID) || string.IsNullOrEmpty(appSettings.AppCertificate))
    {
    return new StatusCodeResult((int)HttpStatusCode.PreconditionFailed);
    }

    var uid = request.uid.ValueKind == JsonValueKind.Number ? request.uid.GetUInt64().ToString() : request.uid.GetString();
    var tokenBuilder = new AccessToken(appSettings.AppID, appSettings.AppCertificate, request.channel, uid);
    tokenBuilder.addPrivilege(Privileges.kJoinChannel, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishAudioStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishVideoStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishDataStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kRtmLogin, request.expiredTs);
    return Ok(new AuthenticateResponse
    {
    channel = request.channel,
    uid = request.uid,
    token = tokenBuilder.build()
    });
    }
    }
    }

    在请求处理中,直接调用了从 AgoraIO 上下载的代码,并且在没有配置 AppID 和 AppCertificate 情况下会回报 412 错误。

    同时,这个示例代码中直接将[kJoinChannel, kPublishAudioStream, kPublishVideoStream, kPubishDataStream, kRtmLogin] 的权限一次性给出来,你可以根据直接的需要,在 AuthenticateRequest 中添加权限申请的字段, 实现权限的申请功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [HttpPost]
    public ActionResult<AuthenticateResponse> index(AuthenticateRequest request)
    {
    if (string.IsNullOrEmpty(appSettings.AppID) || string.IsNullOrEmpty(appSettings.AppCertificate))
    {
    return new StatusCodeResult((int)HttpStatusCode.PreconditionFailed);
    }

    var uid = request.uid.ValueKind == JsonValueKind.Number ? request.uid.GetUInt64().ToString() : request.uid.GetString();
    var tokenBuilder = new AccessToken(appSettings.AppID, appSettings.AppCertificate, request.channel, uid);
    tokenBuilder.addPrivilege(Privileges.kJoinChannel, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishAudioStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishVideoStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kPublishDataStream, request.expiredTs);
    tokenBuilder.addPrivilege(Privileges.kRtmLogin, request.expiredTs);
    return Ok(new AuthenticateResponse
    {
    channel = request.channel,
    uid = request.uid,
    token = tokenBuilder.build()
    });
    }

编译并运行

.NET Core 的编译和运行只需要通过命令行既可以解决,在 AgoraTokenServer 目录下,直接在命令行中运行

1
dotnet build

就可以编译整个工程了。

运行也很直接,直接在命令行中运行

1
dotnet run

就可以在 https://localhost:5001http://localhost:5000 上运行服务了。

如果你想改缺省的运行端口,推荐直接修改 Path: /Properties/launchSettings.json 文件中的 AgoraTokenServer 这一节的 applicationUrl 参数,其内容如下:

1
2
3
4
5
6
7
8
9
"AgoraTokenServer": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}

因为修改过的 launchSettings.json 本身也会做为一个配置文件发布在最终运行目录中,这样子就不用吧端口写死在源代码中,或者在 Program.cs 中额外添加代码了。

测试结果

在本文中,使用Postman对服务进行测试,大家可以使用自己习惯的工具。在具体的请求中,因为 expiredTS 和 role 在程序中有缺省值,所以请求中就可以忽略,并且在现阶段,role 只有一个值,所以推荐可以暂时忽略这个。而 expiredTS 的具体用法,可以参考Agora官方网站的生成Token一文中的说明。

具体的 Postman 请求结果如下图所示。

如果你在使用 Postman 发送请求的时候发生了下图的错误:

是因为你现在访问的 https 链接使用的证书是无效的,实际使用中你需要部署真实的证书,测试阶段你可以通过下图的 Settings 按钮将第一个 Enable SSL certificate verification 关闭

完成

到现在为止,基于 .NET Core 的 Agora Token 服务已经开发完成。在实际使用中,还需要添加安全机制,这个可以根据你自己的具体架构情况进行完善。

.NET Core 的 docker 化可以参考微软的 Docker 容器 这编文章。

本文的所有代码都可以在 GitHub 上下载。

类别 名称 数量 重量 Check
三大件 田野睡袋 1 872 OK
酱铺的泡沫垫 1 230
AMK Bivy 地布 1 101 OK
酱铺二人金字塔 1 581 OK
酱铺背架(简易版) 1 1010 OK
钛空心地丁 15 137 OK
炊具 小姜手作酒精炉 1 15 OK
bushbuddy柴火炉 1 181 OK
铝锅 1 96 OK
Flod-A-Cup 折叠水杯 1 50 OK
3升康迪净水水袋 1 102 OK
象印梦重力500 1 194 OK
鸭嘴兽1升水袋(奶头盖子) 1 35 OK
勺子 1 OK
衣服 smartwood袜子 1 68
速干长裤 1 320 OK
羽绒裤 1 260 OK
冲锋裤 1 72 OK
老鼠神衣 1 272 OK
始祖鸟冲锋衣 1 340 OK
羽绒服 1 340 OK
羊毛帽子 1 78 OK
buff太阳帽 1 30
羊毛内衣 1 202 OK
羊毛内裤 1 168 OK
内裤 1 40 OK
羽绒脚套 1 150 OK
电器 小米一万毫安 1 226 OK
Apple Watch充电器 1 24 OK
紫米Type-C充电线+Lightning转接头+microUSB转接头 1 30 OK
Apple Watch 5 1 63
iPhone 11 1 220
充电器 1 43 OK
头灯 1 34 OK
伏来阳太阳能板 1 207 OK
杂项 牙刷牙膏毛巾 1 90 OK
眼镜 1 23 OK
救生哨、打火棒 1 33 OK
药品 1 OK
睡觉耳塞 1 OK

根据How does a Queue compare to a Topic的介绍, 以下是原文:

Topics
In JMS a Topic implements publish and subscribe semantics. When you publish a message it goes to all the subscribers who are interested - so zero to many subscribers will receive a copy of the message. Only subscribers who had an active subscription at the time the broker receives the message will get a copy of the message.

Queues
A JMS Queue implements load balancer semantics. A single message will be received by exactly one consumer. If there are no consumers available at the time the message is sent it will be kept until a consumer is available that can process the message. If a consumer receives a message and does not acknowledge it before closing then the message will be redelivered to another consumer. A queue can have many consumers with messages load balanced across the available consumers.

So Queues implement a reliable load balancer in JMS.

说明, 以topic形式出现的消息在没有订阅者的情况下, 并不能持久化, 而是直接丢弃了. 只有发布在queue中才能持久化. 其表现为, 通过MQTT发布的topic, 在没有特别配置的情况下, 就是说在topic与queue都是auto-create的情况下, 消息会在没有消费者的情况下直接丢失.

通过配置storker.xml文件, 可以指定某个address生成queue, 比如:

1
2
3
4
5
<address name="sometopic">
<anycast>
<queue name="sometopic" />
</anycast>
</address>

这时, 发送到这个topic上的消息就可以持久化了.

但这里要注意, 如果是:

1
2
3
4
5
<address name="sometopic">
<multicast>
<queue name="sometopic" />
</multicast>
</address>

会形成一个publish/subscribe形式, 根据上文指出的, 就不会持久化消息.

最后, mqtt缺省的就是publish/subscribe模型, 在没有订阅者的情况下就是会丢失消息.

然后没有实现, 去了梅里

类别 名称 数量 重量 Check
三大件 田野睡袋 1 872 OK
therm-a-rest xlite+气泵 1 415 OK
AMK Bivy 地布 1 101 OK
酱铺cuben天幕加吊床 1 681 OK
酱铺背架 1 1010 OK
炊具 SOTO WindMaster 1 49 OK
柴火炉 1 160 OK
铝锅 1 96 OK
Flod-A-Cup 折叠水杯 1 50 OK
3升康迪净水水袋 1 102 OK
膳魔师FEK800 1 194 OK
衣服 smartwood袜子 1 68 OK
outdome袜子 1 60 OK
防沙套 1 40
速干长裤 1 320 OK
羽绒裤 1 260 OK
冲锋裤 1 268 OK
羊毛T恤 1 165 OK
老鼠神衣 1 272 OK
始祖鸟冲锋衣 1 340 OK
羽绒服 1 340 OK
迪卡侬手套 1 103
羊毛帽子 1 78 OK
棒球帽 1 100 OK
羊毛内衣 1 202 OK
羊毛内裤 1 168 OK
内裤 1 40 OK
羽绒胶套 1 150 OK
电器 小米一万毫安 1 226 OK
Garmin充电线 1 15 OK
Type-C充电线 1 30 OK
北斗海聊 233 OK
iPhone 11 1 220 OK
充电器 1 43 OK
头灯 1 34 OK
杂项 牙刷牙膏毛巾 1 90 OK
眼镜 1 23 OK
救生哨、打火棒 1 33 OK
Garmin forunner 935 1 50 OK
三峰bivy 1 140 OK

华为手机的superCharge支持的
超级快充标准是 5V4.5A、4.5V5A
快速充电标准是 9V2A
普通充电标准是 5V2A 5V1A

如果充电线支持的是2A的电流,那么用原装SuperChange充电头,反而只能普通充电模式
充电宝要想快速充电,要支持到9V2A的输出才行,否则只能普通充电模式

Garmin这个奸商,广告里不带导航功能的手表连倒入路径都不支持,就算是准旗舰的forerunner 935。但其实要倒入导航路径很简单。

  1. 登录Garmin Connect,使用“导入”功能来把gpx路径导入到Garmin Connect中。

倒入路径

  1. 选中刚刚倒入的路径

路径详情

  1. 下载路径为FIT格式

FIT格式

  1. 把手表通过USB连接到电脑上,连接模式选择大容量存储

  2. 把下载的FIT文件拷贝到如图的“COURSES”目录下

目录

  1. Well done, and enjoy!