《Android FFmpeg 播放器开发梳理》:

  • 第零章 基础公共类的封装


播放器初始化与解复用流程

这一章,我们来讲解播放器解复用(从文件中读取数据包)的流程。在讲解播放器的读数据包流程之前,我们先定义一个播放器状态结构体,用来记录播放器的各种状态。

播放器状态结构体

首先,我们定义一个结构体,用于记录播放器的开始、暂停、定位等各种状态标志,以及重置结构体的方法:

 1/**2 * 播放器状态结构体3 */4typedef struct PlayerState {56    AVDictionary *sws_dict;         // 视频转码option参数7    AVDictionary *swr_opts;         //8    AVDictionary *format_opts;      // 解复用option参数9    AVDictionary *codec_opts;       // 解码option参数
10    AVDictionary *resample_opts;    // 重采样option参数
11
12    const char *audioCodecName;     // 指定音频解码器名称
13    const char *videoCodecName;     // 指定视频解码器名称
14
15    int abortRequest;               // 退出标志
16    int pauseRequest;               // 暂停标志
17    SyncType syncType;              // 同步类型
18    int64_t startTime;              // 播放起始位置
19    int64_t duration;               // 播放时长
20    int realTime;                   // 判断是否实时流
21    int audioDisable;               // 是否禁止音频流
22    int videoDisable;               // 是否禁止视频流
23    int displayDisable;             // 是否禁止显示
24
25    int fast;                       // 解码上下文的AV_CODEC_FLAG2_FAST标志
26    int genpts;                     // 解码上下文的AVFMT_FLAG_GENPTS标志
27    int lowres;                     // 解码上下文的lowres标志
28
29    float playbackRate;             // 播放速度
30    float playbackPitch;            // 播放音调
31
32    int seekByBytes;                // 是否以字节定位
33    int seekRequest;                // 定位请求
34    int seekFlags;                  // 定位标志
35    int64_t seekPos;                // 定位位置
36    int64_t seekRel;                // 定位偏移
37
38    int autoExit;                   // 是否自动退出
39    int loop;                       // 循环播放
40    int mute;                       // 静音播放
41    int frameDrop;                  // 舍帧操作
42
43} PlayerState;
44
45/**
46 * 重置播放器状态结构体
47 * @param state
48 */
49inline void resetPlayerState(PlayerState *state) {
50
51    av_opt_free(state);
52
53    av_dict_free(&state->sws_dict);
54    av_dict_free(&state->swr_opts);
55    av_dict_free(&state->format_opts);
56    av_dict_free(&state->codec_opts);
57    av_dict_free(&state->resample_opts);
58    av_dict_set(&state->sws_dict, "flags", "bicubic", 0);
59
60    if (state->audioCodecName != NULL) {
61        av_freep(&state->audioCodecName);
62        state->audioCodecName = NULL;
63    }
64    if (state->videoCodecName != NULL) {
65        av_freep(&state->videoCodecName);
66        state->videoCodecName = NULL;
67    }
68    state->abortRequest = 1;
69    state->pauseRequest = 0;
70    state->seekByBytes = 0;
71    state->syncType = AV_SYNC_AUDIO;
72    state->startTime = AV_NOPTS_VALUE;
73    state->duration = AV_NOPTS_VALUE;
74    state->realTime = 0;
75    state->audioDisable = 0;
76    state->videoDisable = 0;
77    state->displayDisable = 0;
78    state->fast = 0;
79    state->genpts = 0;
80    state->lowres = 0;
81    state->playbackRate = 1.0;
82    state->playbackPitch = 1.0;
83    state->seekRequest = 0;
84    state->seekFlags = 0;
85    state->seekPos = 0;
86    state->seekRel = 0;
87    state->seekRel = 0;
88    state->autoExit = 0;
89    state->loop = 0;
90    state->frameDrop = 0;
91}

播放器状态结构体的作用是用来给解复用、解码、同步等流程共享状态的,方便统一播放器的状态。

初始化以及解复用

我们在播放器调用 prepare() 时创建一个线程用来初始化解码器、打开音视频输出设备和音视频同步渲染线程等出来了,在准备完成后,等待播放器调用 start() 方法更新PlayerState 的 pauseRequest 标志,进入读数据包流程。

读数据包线程执行逻辑如下:

初始化流程

  1. 利用avformat_alloc_context()方法创建解复用上下文并设置解复用中断回调

  2. 利用 avformat_open_input()方法打开url,url可以是本地文件,也可以使网络媒体流

  3. 在成功打开文件之后,我们需要利用avformat_find_stream_info()方法查找媒体流信息

  4. 如果开始播放的位置不是AV_NOPTS_VALUE,即从文件开头开始的话,需要先利用avformat_seek_file方法定位到播放的起始位置

  5. 查找音频流、视频流索引,然后根据是否禁用音频流、视频流判断的设置,分别准备解码器对象

  6. 当我们准备好解码器之后,通过媒体播放器回调播放器已经准备。

  7. 判断音频解码器是否存在,通过openAudioDevice方法打开音频输出设备

  8. 判断视频解码器是否存在,打开视频同步输出设备

其中,第7、第8步骤,我们需要根据实际情况,重新设定同步类型。同步有三种类型,同步到音频时钟、同步到视频时钟、同步到外部时钟。默认是同步到音频时钟,如果音频解码器不存在,则根据需求同步到视频时钟还是外部时钟。

解复用流程

经过前面的初始化之后,我们就可以进入读数据包流程。读数据包流程如下:

  1. 判断是否退出播放器

  2. 判断暂停状态是否发生改变,设置解复用是否暂停还是播放 —— av_read_pause 和 av_read_play

  3. 处理定位状态。利用avformat_seek_file()方法定位到实际的位置,如果定位成功,我们需要清空音频解码器、视频解码器中待解码队列的数据。处理完前面的逻辑,我们需要更新外部时钟,并且更新视频同步刷新的时钟

  4. 根据是否设置attachmentRequest标志,从视频流取出attached_pic的数据包

  5. 判断解码器待解码队列的数据包如果大于某个数量,则等待解码器消耗

  6. 读数据包

  7. 判断数据包是否读取成功。如果没成功,则判断读取的结果是否AVERROR_EOF,即结尾标志。如果到了结尾,则入队一个空的数据包。如果读取出错,则直接退出读数据包流程。如果都不是,则判断解码器中的待解码数据包、待输出帧是否存在数据,如果都不存在数据,则判断是否跳转至起始位置还是判断是否自动退出,或者是继续下一轮读数据包流程。

  8. 根据取得的数据包判断是否在播放范围内的数据包。如果在播放范围内,则根据数据包的媒体流索引判断是否入队数据包舍弃。

至此,解复用流程就完成了。

整个线程执行体的代码如下:

  1int MediaPlayer::readPackets() {2    int ret = 0;3    AVDictionaryEntry *t;4    AVDictionary **opts;5    int scan_all_pmts_set = 0;67    // 准备解码器8    mMutex.lock();9    do {10        // 创建解复用上下文11        pFormatCtx = avformat_alloc_context();12        if (!pFormatCtx) {13            av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");14            ret = AVERROR(ENOMEM);15            break;16        }1718        // 设置解复用中断回调19        pFormatCtx->interrupt_callback.callback = avformat_interrupt_cb;20        pFormatCtx->interrupt_callback.opaque = playerState;21        if (!av_dict_get(playerState->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {22            av_dict_set(&playerState->format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);23            scan_all_pmts_set = 1;24        }2526        // 设置rtmp/rtsp的超时值27        if (av_stristart(url, "rtmp", NULL) || av_stristart(url, "rtsp", NULL)) {28            // There is total different meaning for 'timeout' option in rtmp29            av_log(NULL, AV_LOG_WARNING, "remove 'timeout' option for rtmp.\n");30            av_dict_set(&playerState->format_opts, "timeout", NULL, 0);31        }3233        // 打开文件34        ret = avformat_open_input(&pFormatCtx, url, NULL, &playerState->format_opts);35        if (ret < 0) {36            printError(url, ret);37            ret = -1;38            break;39        }4041        if (scan_all_pmts_set) {42            av_dict_set(&playerState->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);43        }4445        if ((t = av_dict_get(playerState->format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {46            av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);47            ret = AVERROR_OPTION_NOT_FOUND;48            break;49        }5051        if (playerState->genpts) {52            pFormatCtx->flags |= AVFMT_FLAG_GENPTS;53        }54        av_format_inject_global_side_data(pFormatCtx);5556        opts = setupStreamInfoOptions(pFormatCtx, playerState->codec_opts);5758        // 查找媒体流信息59        ret = avformat_find_stream_info(pFormatCtx, opts);60        if (opts != NULL) {61            for (int i = 0; i < pFormatCtx->nb_streams; i++) {62                if (opts[i] != NULL) {63                    av_dict_free(&opts[i]);64                }65            }66            av_freep(&opts);67        }6869        if (ret < 0) {70            av_log(NULL, AV_LOG_WARNING,71                   "%s: could not find codec parameters\n", url);72            ret = -1;73            break;74        }7576        // 文件时长(秒)77        if (pFormatCtx->duration != AV_NOPTS_VALUE) {78            mDuration = (int)(pFormatCtx->duration / AV_TIME_BASE);79        }8081        if (pFormatCtx->pb) {82            pFormatCtx->pb->eof_reached = 0;83        }84        // 判断是否以字节方式定位85        playerState->seekByBytes = !!(pFormatCtx->iformat->flags & AVFMT_TS_DISCONT)86                                     && strcmp("ogg", pFormatCtx->iformat->name);8788        // 设置最大帧间隔89        mediaSync->setMaxDuration((pFormatCtx->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0);9091        // 如果不是从头开始播放,则跳转到播放位置92        if (playerState->startTime != AV_NOPTS_VALUE) {93            int64_t timestamp;9495            timestamp = playerState->startTime;96            if (pFormatCtx->start_time != AV_NOPTS_VALUE) {97                timestamp += pFormatCtx->start_time;98            }99            ret = avformat_seek_file(pFormatCtx, -1, INT64_MIN, timestamp, INT64_MAX, 0);
100            if (ret < 0) {
101                av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",
102                       url, (double)timestamp / AV_TIME_BASE);
103            }
104        }
105        // 判断是否实时流,判断是否需要设置无限缓冲区
106        playerState->realTime = isRealTime(pFormatCtx);
107        if (playerState->infiniteBuffer < 0 && playerState->realTime) {
108            playerState->infiniteBuffer = 1;
109        }
110
111        // 查找媒体流信息
112        int audioIndex = -1;
113        int videoIndex = -1;
114        for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
115            if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
116                if (audioIndex == -1) {
117                    audioIndex = i;
118                }
119            } else if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
120                if (videoIndex == -1) {
121                    videoIndex = i;
122                }
123            }
124        }
125        // 如果不禁止视频流,则查找最合适的视频流索引
126        if (!playerState->videoDisable) {
127            videoIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO,
128                                             videoIndex, -1, NULL, 0);
129        } else {
130            videoIndex = -1;
131        }
132        // 如果不禁止音频流,则查找最合适的音频流索引(与视频流关联的音频流)
133        if (!playerState->audioDisable) {
134            audioIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_AUDIO,
135                                             audioIndex, videoIndex, NULL, 0);
136        } else {
137            audioIndex = -1;
138        }
139
140        // 如果音频流和视频流都没有找到,则直接退出
141        if (audioIndex == -1 && videoIndex == -1) {
142            av_log(NULL, AV_LOG_WARNING,
143                   "%s: could not find audio and video stream\n", url);
144            ret = -1;
145            break;
146        }
147
148        // 根据媒体流索引准备解码器
149        if (audioIndex >= 0) {
150            prepareDecoder(audioIndex);
151        }
152        if (videoIndex >= 0) {
153            prepareDecoder(videoIndex);
154        }
155
156        if (!audioDecoder && !videoDecoder) {
157            av_log(NULL, AV_LOG_WARNING,
158                   "failed to create audio and video decoder\n");
159            ret = -1;
160            break;
161        }
162        ret = 0;
163    } while (false);
164    mMutex.unlock();
165
166    // 出错返回
167    if (ret < 0) {
168        if (playerCallback) {
169            playerCallback->onError(0x01, "prepare decoder failed!");
170        }
171        return -1;
172    }
173
174    // 准备完成回调
175    if (playerCallback) {
176        playerCallback->onPrepared();
177    }
178
179    // 视频解码器开始解码
180    if (videoDecoder != NULL) {
181        videoDecoder->start();
182    } else {
183        if (playerState->syncType == AV_SYNC_VIDEO) {
184            playerState->syncType = AV_SYNC_AUDIO;
185        }
186    }
187
188    // 音频解码器开始解码
189    if (audioDecoder != NULL) {
190        audioDecoder->start();
191    } else {
192        if (playerState->syncType == AV_SYNC_AUDIO) {
193            playerState->syncType = AV_SYNC_EXTERNAL;
194        }
195    }
196
197    // 打开音频输出设备
198    if (audioDecoder != NULL) {
199        AVCodecContext *avctx = audioDecoder->getCodecContext();
200        ret = openAudioDevice(avctx->channel_layout, avctx->channels,
201                        avctx->sample_rate);
202        if (ret < 0) {
203            av_log(NULL, AV_LOG_WARNING, "could not open audio device\n");
204            // 如果音频设备打开失败,则调整时钟的同步类型
205            if (playerState->syncType == AV_SYNC_AUDIO) {
206                if (videoDecoder != NULL) {
207                    playerState->syncType = AV_SYNC_VIDEO;
208                } else {
209                    playerState->syncType = AV_SYNC_EXTERNAL;
210                }
211            }
212        } else {
213            // 启动音频输出设备
214            audioDevice->start();
215        }
216    }
217
218    if (videoDecoder) {
219        if (playerState->syncType == AV_SYNC_AUDIO) {
220            videoDecoder->setMasterClock(mediaSync->getAudioClock());
221        } else if (playerState->syncType == AV_SYNC_VIDEO) {
222            videoDecoder->setMasterClock(mediaSync->getVideoClock());
223        } else {
224            videoDecoder->setMasterClock(mediaSync->getExternalClock());
225        }
226    }
227
228    // 开始同步
229    mediaSync->start(videoDecoder, audioDecoder);
230
231    // 读数据包流程
232    eof = 0;
233    ret = 0;
234    AVPacket pkt1, *pkt = &pkt1;
235    int64_t stream_start_time;
236    int playInRange = 0;
237    int64_t pkt_ts;
238    for (;;) {
239
240        // 退出播放器
241        if (playerState->abortRequest) {
242            break;
243        }
244
245        // 是否暂停
246        if (playerState->pauseRequest != lastPaused) {
247            lastPaused = playerState->pauseRequest;
248            if (playerState->pauseRequest) {
249                av_read_pause(pFormatCtx);
250            } else {
251                av_read_play(pFormatCtx);
252            }
253        }
254
255#if CONFIG_RTSP_DEMUXER || CONFIG_MMSH_PROTOCOL
256        if (playerState->pauseRequest &&
257            (!strcmp(pFormatCtx->iformat->name, "rtsp") ||
258             (pFormatCtx->pb && !strncmp(url, "mmsh:", 5)))) {
259            continue;
260        }
261#endif
262        // 定位处理
263        if (playerState->seekRequest) {
264            int64_t seek_target = playerState->seekPos;
265            int64_t seek_min = playerState->seekRel > 0 ? seek_target - playerState->seekRel + 2: INT64_MIN;
266            int64_t seek_max = playerState->seekRel < 0 ? seek_target - playerState->seekRel - 2: INT64_MAX;
267            // 定位
268            ret = avformat_seek_file(pFormatCtx, -1, seek_min, seek_target, seek_max, playerState->seekFlags);
269            if (ret < 0) {
270                av_log(NULL, AV_LOG_ERROR, "%s: error while seeking\n", url);
271            } else {
272                if (audioDecoder) {
273                    audioDecoder->flush();
274                }
275                if (videoDecoder) {
276                    videoDecoder->flush();
277                }
278
279                if (audioDevice) {
280                    audioDevice->flush();
281                }
282
283                // 更新外部时钟值
284                if (playerState->seekFlags & AVSEEK_FLAG_BYTE) {
285                    mediaSync->updateExternalClock(NAN);
286                } else {
287                    mediaSync->updateExternalClock(seek_target / (double)AV_TIME_BASE);
288                }
289                mediaSync->refreshVideoTimer();
290            }
291            attachmentRequest = 1;
292            playerState->seekRequest = 0;
293            eof = 0;
294        }
295
296        // 取得封面数据包
297        if (attachmentRequest) {
298            if (videoDecoder && (videoDecoder->getStream()->disposition
299                                 & AV_DISPOSITION_ATTACHED_PIC)) {
300                AVPacket copy;
301                if ((ret = av_copy_packet(&copy, &videoDecoder->getStream()->attached_pic)) < 0) {
302                    break;
303                }
304                videoDecoder->pushPacket(&copy);
305                videoDecoder->pushNullPacket();
306            }
307            attachmentRequest = 0;
308        }
309
310        // 如果队列中存在足够的数据包,则等待消耗
311        // 备注:这里要等待一定时长的缓冲队列,要不然会导致OpenSLES播放音频出现卡顿等现象
312        if (playerState->infiniteBuffer < 1 &&
313            ((audioDecoder ? audioDecoder->getMemorySize() : 0) + (videoDecoder ? videoDecoder->getMemorySize() : 0) > MAX_QUEUE_SIZE
314             || (!audioDecoder || audioDecoder->hasEnoughPackets()) && (!videoDecoder || videoDecoder->hasEnoughPackets()))) {
315            continue;
316        }
317
318        // 读出数据包
319        ret = av_read_frame(pFormatCtx, pkt);
320        if (ret < 0) {
321            // 如果没能读出裸数据包,判断是否是结尾
322            if ((ret == AVERROR_EOF || avio_feof(pFormatCtx->pb)) && !eof) {
323                if (videoDecoder != NULL) {
324                    videoDecoder->pushNullPacket();
325                }
326                if (audioDecoder != NULL) {
327                    audioDecoder->pushNullPacket();
328                }
329                eof = 1;
330            }
331            // 读取出错,则直接退出
332            if (pFormatCtx->pb && pFormatCtx->pb->error) {
333                ret = -1;
334                break;
335            }
336
337            // 如果不处于暂停状态,并且队列中所有数据都没有,则判断是否需要
338            if (!playerState->pauseRequest && (!audioDecoder || audioDecoder->getPacketSize() == 0)
339                && (!videoDecoder || (videoDecoder->getPacketSize() == 0
340                                      && videoDecoder->getFrameSize() == 0))) {
341                if (playerState->loop) {
342                    seekTo(playerState->startTime != AV_NOPTS_VALUE ? playerState->startTime : 0);
343                } else if (playerState->autoExit) {
344                    ret = AVERROR_EOF;
345                    break;
346                }
347            }
348            continue;
349        } else {
350            eof = 0;
351        }
352
353        // 计算pkt的pts是否处于播放范围内
354        stream_start_time = pFormatCtx->streams[pkt->stream_index]->start_time;
355        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
356        // 播放范围
357        playInRange = playerState->duration == AV_NOPTS_VALUE
358                      || (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
359                         av_q2d(pFormatCtx->streams[pkt->stream_index]->time_base)
360                         - (double)(playerState->startTime != AV_NOPTS_VALUE ? playerState->startTime : 0) / 1000000
361                         <= ((double)playerState->duration / 1000000);
362        if (playInRange && audioDecoder && pkt->stream_index == audioDecoder->getStreamIndex()) {
363            audioDecoder->pushPacket(pkt);
364        } else if (playInRange && videoDecoder && pkt->stream_index == videoDecoder->getStreamIndex()) {
365            videoDecoder->pushPacket(pkt);
366        } else {
367            av_packet_unref(pkt);
368        }
369    }
370
371    if (ret < 0) {
372        if (playerCallback) {
373            playerCallback->onError(0x02, "error when reading packets!");
374        }
375    } else { // 播放完成
376        if (playerCallback) {
377            playerCallback->onComplete();
378        }
379    }
380
381    ALOGD("read packets thread exit!");
382    return ret;
383}

完整代码可以参考作者的播放器项目:

https://github.com/CainKernel/CainPlayer

推荐阅读

  • (强烈推荐)移动端音视频从零到上手(上)

  • 跨平台渲染引擎之路:拨云见日

  • Google Jetpack 新组件 CameraX 介绍与实践

  • OpenGL ES 学习资源分享


关注微信公众号【纸上浅谈】,Android开发、音视频、Camera、OpenGL、NDK 开发相关文章~~~

《Android FFmpeg 播放器开发梳理》第一章 播放器初始化与解复用流程相关推荐

  1. Android群英传神兵利器读书笔记——第一章:程序员小窝——搭建高效的开发环境

    Android群英传神兵利器读书笔记--第一章:程序员小窝--搭建高效的开发环境 目录 1.1 搭建高效的开发环境之操作系统 1.2 搭建开发环境之高效配置 基本环境配置 基本开发工具 1.3 搭建程 ...

  2. Android FFmpeg 音视频开发教程

    LearnFFmpeg 项目地址:githubhaohao/LearnFFmpeg 简介: Android FFmpeg 音视频开发教程 更多:作者   提 Bug 标签: An Android FF ...

  3. 路飞学城python电子书_路飞学城-Python开发集训-第一章

    路飞学城-Python开发集训-第一章 1.本章学习心得.体会 我: 间接性勤奋. 我: 学习方法论:输入--输出---纠正 我: 对对对 走出舒适区, 换圈子, 转思路,投资自我加筹码. 我: 圈子 ...

  4. 人工操作阶段计算机是如何工作的,第一章计算机基础概述全解.ppt

    第一章计算机基础概述全解 1.2.3 汉字编码 汉字的编码 国标码:中文内码之一,汉字信息交换的标准编码.国标码是不可能在计算机内部直接采用.于是, 汉字的机内码采用变形国标码 . 国标码:作为转换为 ...

  5. python应用开发实战第一章 兽人之袭0.0.1

    第一章:采用面向对象编程实现兽人之袭文本游戏 1.采用面向对象编程实现 # python应用开发实战 #兽人之袭v1.0.面向对象编程 ''' 需求分析: 1.获得所有木屋击败木屋里的所有敌人 2.可 ...

  6. Unix/Linux下的Curses库开发指南——第一章 Curses库开发简介

    1.1什么是curses curses实际上是一个函数开发包,专门用来进行UNIX下终端环境下的屏幕界面处理以及I/O处理.通过这些函数库,C和C++程序就可以控制终端的视频显示以及输入输出.使用cu ...

  7. Android 资讯类App项目实战 第一章 滑动顶部导航栏

    前言: 正在做一个资讯类app,打算一边做一边整理,供自己学习与巩固.用到的知识复杂度不高,仅适于新手.经验不多,如果写出来的代码有不好的地方欢迎讨论. 该系列的其他文章 第二章 retrofit获取 ...

  8. 计算机操作系统(第四版)知识点梳理——第一章

    计算机操作系统(汤晓丹) 第一章 操作系统引论 1.1 操作系统的目标和作用 1.2 操作系统的发展过程 1.3 操作系统的基本特性 1.4 操作系统的主要功能 1.5 OS结构设计 1.6 计算机硬 ...

  9. 《Openwrt开发》第一章:newifi3 刷自己编译的Openwrt固件

    最近在淘宝入手了一个二手的newifi3,主要是因为它内存大,而且性价比相当高,512M的ddr2和32M的flash买下来才100左右. 好了,废话不多说,开始第一章的源码编译征程. 1.准备 源码 ...

最新文章

  1. 用coffee和socket.io实现的01背包算法
  2. MOS管好坏的判别方法
  3. .net 深入系统编程(三)
  4. 使用git remote提交代码
  5. xcode清除最近打开的文件列表
  6. 额!Java中用户线程和守护线程区别这么大?
  7. STM32之串口例程
  8. 慢日志之一:开启mysql慢查询日志并使用mysqldumpslow命令查看,分析诊断工具之四...
  9. 工作中的注意事项、细节
  10. 矩阵的乘法与利用矩阵求解线性方程组
  11. 和平精英android怎么写符号,特殊符号输入方法 和平精英iOS和安卓名字特殊符号...
  12. 易语言教程数据库删除命令
  13. ubuntu护眼软件——Redshift
  14. linux opengl安装教程,求OpenGL安装过程
  15. win10查看端口号、进程
  16. 宽带无法远程连接到计算机,登录校园宽带是显示不能建立远程计算机连接,在别的电脑可以登录 是为什么?...
  17. java计算机毕业设计旅游网站源程序+mysql+系统+lw文档+远程调试
  18. debian 9 配置ati驱动
  19. 电子设计解决方案透视
  20. 数据通信,数据通信原理是什么?

热门文章

  1. 使用Ezy-Slice插件实现类似Beat Saber的模型切割效果(一)
  2. Java 数字转汉字工具类
  3. 163邮箱苹果设置不成功_怎么样才能让自己服务器发出的邮件不被 Gmail、Hotmail、163、QQ 等邮箱放入垃圾箱...
  4. SDS(Spoken Dialogue System) 对话系统
  5. java httpclient cdn_Java 11`HttpClient`下载但不是吗? (负内容长度)
  6. SQL Server笔记心得(持续更新)
  7. Matlab画根轨迹
  8. Linux软件安装失败问题,source.list用了bionic,实际上我的Linux是focal版本
  9. 爬虫:Xpath定位
  10. c8051f020 I/O配置小结