在这里简单介绍一下,如何利用Android MediaCodec解码AAC音频文件或者实时AAC音频帧并通过AudioTrack来播放。主要的思路就是从文件或者网络获取一帧帧的AAC的数据,送入解码器解码后播放。

封装AudioTrack

AudioTrack主要是用来进行主要是用来播放声音的,但是只能播放PCM格式的音频流。这里主要是简单的对AudioTrack进行了封装,加入了一些异常判断:

/*** Created by ZhangHao on 2017/5/10.* 播放pcm数据*/
public class MyAudioTrack {private int mFrequency;// 采样率private int mChannel;// 声道private int mSampBit;// 采样精度private AudioTrack mAudioTrack;public MyAudioTrack(int frequency, int channel, int sampbit) {this.mFrequency = frequency;this.mChannel = channel;this.mSampBit = sampbit;}/*** 初始化*/public void init() {if (mAudioTrack != null) {release();}// 获得构建对象的最小缓冲区大小int minBufSize = getMinBufferSize();mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM);mAudioTrack.play();}/*** 释放资源*/public void release() {if (mAudioTrack != null) {mAudioTrack.stop();mAudioTrack.release();}}/*** 将解码后的pcm数据写入audioTrack播放** @param data   数据* @param offset 偏移* @param length 需要播放的长度*/public void playAudioTrack(byte[] data, int offset, int length) {if (data == null || data.length == 0) {return;}try {mAudioTrack.write(data, offset, length);} catch (Exception e) {Log.e("MyAudioTrack", "AudioTrack Exception : " + e.toString());}}public int getMinBufferSize() {return AudioTrack.getMinBufferSize(mFrequency,mChannel, mSampBit);}
}
  • 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
  • 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

这里简单介绍一下,在AudioTrack构造方法AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)里几个变量的含义: 
1.streamType:指定流的类型,主要包括以下几种: 
- STREAM_ALARM:警告声 
- STREAM_MUSCI:音乐声 
- STREAM_RING:铃声 
- STREAM_SYSTEM:系统声音 
- STREAM_VOCIE_CALL:电话声音 
因为android系统对不同的声音的管理是分开的,所以这个参数的作用就是设置AudioTrack播放的声音类型。

2.sampleRateInHz : 采样率

3.channelConfig : 声道

4.audioFormat : 采样精度

5.bufferSizeInBytes :缓冲区大小,可以通过AudioTrack.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)来获取

6.mode : MODE_STATIC和MODE_STREAM: 
- MODE_STATIC : 直接把所有的数据加载到缓存区,不需要多次write,一般用于占用内存小,延时要求高的情况 
- MODE_STREAM : 需要多次write,一般用于像从网络获取数据或者实时解码的情况,本次的例子就是这种情况。

我这里只是简单的介绍,大家可以去网上找更为详细的介绍。

AAC解码器

这里主要对MediaCodec进行封装,实现一帧帧去解码AAC。

/*** Created by ZhangHao on 2017/5/17.* 用于aac音频解码*/public class AACDecoderUtil {private static final String TAG = "AACDecoderUtil";//声道数private static final int KEY_CHANNEL_COUNT = 2;//采样率private static final int KEY_SAMPLE_RATE = 48000;//用于播放解码后的pcmprivate MyAudioTrack mPlayer;//解码器private MediaCodec mDecoder;//用来记录解码失败的帧数private int count = 0;/*** 初始化所有变量*/public void start() {prepare();}/*** 初始化解码器** @return 初始化失败返回false,成功返回true*/public boolean prepare() {// 初始化AudioTrackmPlayer = new MyAudioTrack(KEY_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);mPlayer.init();try {//需要解码数据的类型String mine = "audio/mp4a-latm";//初始化解码器mDecoder = MediaCodec.createDecoderByType(mine);//MediaFormat用于描述音视频数据的相关参数MediaFormat mediaFormat = new MediaFormat();//数据类型mediaFormat.setString(MediaFormat.KEY_MIME, mine);//声道个数mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, KEY_CHANNEL_COUNT);//采样率mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, KEY_SAMPLE_RATE);//比特率mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);//用来标记AAC是否有adts头,1->有mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);//用来标记aac的类型mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);//ByteBuffer key(暂时不了解该参数的含义,但必须设置)byte[] data = new byte[]{(byte) 0x11, (byte) 0x90};ByteBuffer csd_0 = ByteBuffer.wrap(data);mediaFormat.setByteBuffer("csd-0", csd_0);//解码器配置mDecoder.configure(mediaFormat, null, null, 0);} catch (IOException e) {e.printStackTrace();return false;}if (mDecoder == null) {return false;}mDecoder.start();return true;}/*** aac解码+播放*/public void decode(byte[] buf, int offset, int length) {//输入ByteBufferByteBuffer[] codecInputBuffers = mDecoder.getInputBuffers();//输出ByteBufferByteBuffer[] codecOutputBuffers = mDecoder.getOutputBuffers();//等待时间,0->不等待,-1->一直等待long kTimeOutUs = 0;try {//返回一个包含有效数据的input buffer的index,-1->不存在int inputBufIndex = mDecoder.dequeueInputBuffer(kTimeOutUs);if (inputBufIndex >= 0) {//获取当前的ByteBufferByteBuffer dstBuf = codecInputBuffers[inputBufIndex];//清空ByteBufferdstBuf.clear();//填充数据dstBuf.put(buf, offset, length);//将指定index的input buffer提交给解码器mDecoder.queueInputBuffer(inputBufIndex, 0, length, 0, 0);}//编解码器缓冲区MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();//返回一个output buffer的index,-1->不存在int outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);if (outputBufferIndex < 0) {//记录解码失败的次数count++;}ByteBuffer outputBuffer;while (outputBufferIndex >= 0) {//获取解码后的ByteBufferoutputBuffer = codecOutputBuffers[outputBufferIndex];//用来保存解码后的数据byte[] outData = new byte[info.size];outputBuffer.get(outData);//清空缓存outputBuffer.clear();//播放解码后的数据mPlayer.playAudioTrack(outData, 0, info.size);//释放已经解码的buffermDecoder.releaseOutputBuffer(outputBufferIndex, false);//解码未解完的数据outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);}} catch (Exception e) {Log.e(TAG, e.toString());e.printStackTrace();}}//返回解码失败的次数public int getCount() {return count;}/*** 释放资源*/public void stop() {try {if (mPlayer != null) {mPlayer.release();mPlayer = null;}if (mDecoder != null) {mDecoder.stop();mDecoder.release();}} catch (Exception e) {e.printStackTrace();}}
}
  • 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
  • 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

其实这里和我之前利用MediaCodec解码H264很类似,主要就是在因为解码数据类型不同,所以初始化时有区别。还有一点就是解码H624时,直接将解码后数据利用surface显示,而解码aac是将解码后的数据取出来,再利用AudioTrack播放。

读取aac文件

这里是利用线程读aac文件,获得一帧帧的aac帧数据,然后送入解码器播放。

/*** Created by ZhangHao on 2017/4/18.* 播放aac音频文件*/
public class ReadAACFileThread extends Thread {//音频解码器private AACDecoderUtil audioUtil;//文件路径private String filePath;//文件读取完成标识private boolean isFinish = false;//这个值用于找到第一个帧头后,继续寻找第二个帧头,如果解码失败可以尝试缩小这个值private int FRAME_MIN_LEN = 50;//一般AAC帧大小不超过200k,如果解码失败可以尝试增大这个值private static int FRAME_MAX_LEN = 100 * 1024;//根据帧率获取的解码每帧需要休眠的时间,根据实际帧率进行操作private int PRE_FRAME_TIME = 1000 / 50;//记录获取的帧数private int count = 0;public ReadAACFileThread(String path) {this.audioUtil = new AACDecoderUtil();this.filePath = path;this.audioUtil.start();}@Overridepublic void run() {super.run();File file = new File(filePath);//判断文件是否存在if (file.exists()) {try {FileInputStream fis = new FileInputStream(file);//保存完整数据帧byte[] frame = new byte[FRAME_MAX_LEN];//当前帧长度int frameLen = 0;//每次从文件读取的数据byte[] readData = new byte[10 * 1024];//开始时间long startTime = System.currentTimeMillis();//循环读取数据while (!isFinish) {if (fis.available() > 0) {int readLen = fis.read(readData);//当前长度小于最大值if (frameLen + readLen < FRAME_MAX_LEN) {//将readData拷贝到frameSystem.arraycopy(readData, 0, frame, frameLen, readLen);//修改frameLenframeLen += readLen;//寻找第一个帧头int headFirstIndex = findHead(frame, 0, frameLen);while (headFirstIndex >= 0 && isHead(frame, headFirstIndex)) {//寻找第二个帧头int headSecondIndex = findHead(frame, headFirstIndex + FRAME_MIN_LEN, frameLen);//如果第二个帧头存在,则两个帧头之间的就是一帧完整的数据if (headSecondIndex > 0 && isHead(frame, headSecondIndex)) {//视频解码count++;Log.e("ReadAACFileThread", "Length : " + (headSecondIndex - headFirstIndex));audioUtil.decode(frame, headFirstIndex, headSecondIndex - headFirstIndex);//截取headSecondIndex之后到frame的有效数据,并放到frame最前面byte[] temp = Arrays.copyOfRange(frame, headSecondIndex, frameLen);System.arraycopy(temp, 0, frame, 0, temp.length);//修改frameLen的值frameLen = temp.length;//线程休眠sleepThread(startTime, System.currentTimeMillis());//重置开始时间startTime = System.currentTimeMillis();//继续寻找数据帧headFirstIndex = findHead(frame, 0, frameLen);} else {//找不到第二个帧头headFirstIndex = -1;}}} else {//如果长度超过最大值,frameLen置0frameLen = 0;}} else {//文件读取结束isFinish = true;}}} catch (Exception e) {e.printStackTrace();}Log.e("ReadAACFileThread", "AllCount:" + count + "Error Count : " + audioUtil.getCount());} else {Log.e("ReadH264FileThread", "File not found");}audioUtil.stop();}/*** 寻找指定buffer中AAC帧头的开始位置** @param startIndex 开始的位置* @param data       数据* @param max        需要检测的最大值* @return*/private int findHead(byte[] data, int startIndex, int max) {int i;for (i = startIndex; i <= max; i++) {//发现帧头if (isHead(data, i))break;}//检测到最大值,未发现帧头if (i == max) {i = -1;}return i;}/*** 判断aac帧头*/private boolean isHead(byte[] data, int offset) {boolean result = false;if (data[offset] == (byte) 0xFF && data[offset + 1] == (byte) 0xF1&& data[offset + 3] == (byte) 0x80) {result = true;}return result;}//修眠private void sleepThread(long startTime, long endTime) {//根据读文件和解码耗时,计算需要休眠的时间long time = PRE_FRAME_TIME - (endTime - startTime);if (time > 0) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}}
}
  • 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
  • 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

这里没有太多的东西,就是通过帧头来判断aac帧,并截取每帧数据送入解码器。我这里只是取巧做了简单的判断,对帧头的判断并不一定满足所有的aac帧头,大家可以根据实际的情况自行修改。

结语

其实,实现分离音频帧,利用MediaExtractor这个类就可以实现,但是因为我实际的数据源是来自网络,所以才会demo才会复杂一点。

Android MediaCodec硬解码AAC音频文件并播放相关推荐

  1. Android MediaCodec硬解码AAC音频文件(实时AAC音频帧)并播放

    转载请注明出处:http://blog.csdn.net/a512337862/article/details/72629755 今天在这里简单介绍一下,如何利用android MediaCodec解 ...

  2. Android音频开发(七)音频编解码之MediaCodec编解码AAC下

    在上一篇初识MediaCodec中,我们认识了MediaCodec,知道了MediaCodec的基本工作流程和开发注意事项,这一篇我将讲述如何利用MediaCodec编解码AAC. 1:MediaCo ...

  3. Android使用MediaCodec硬解码播放H264格式视频文件

    前些时间,通过各种搜索加请教了好几个同行的朋友,在他们的指点下实现: RTSP+H264实时视频播放播放及把实时视频流保存到手机SD卡中,再对保存的H264格式文件进行播放等基本功能.非常感谢这些朋友 ...

  4. MediaCodec在Android视频硬解码组件的应用

    https://yq.aliyun.com/articles/632892 云栖社区> 博客列表> 正文 MediaCodec在Android视频硬解码组件的应用 cheenc 2018- ...

  5. 解决ffmpeg获取AAC音频文件duration不准

    最近测试提出了一个bug,ijk获取到的aac文件的duration不准,发来一看,确实不准,在AE或者系统mediaplayer中得到的都是3m48s(准确时间是MMParserExtractor: ...

  6. ffmpeg系列-解决ffmpeg获取aac音频文件duration不准

    这个问题是这样产生的,一同事反应会随机出现ijk获取到的aac文件的duration不准,发来一看,确实不准,在AE或者系统mediaplayer中得到的都是8.4秒(准确时间是MtkAACExtra ...

  7. PotPlayer播放蓝光片源及如何硬解码和音频源码输出

    如何用PotPlayer播放蓝光片源及如何硬解码和音频源码输出 一:什么是PotPlayer ​ PotPlayer 是 KMPlayer 的原制作者姜龙喜先生(韩国)进入 Daum 公司后的新一代网 ...

  8. Android 入门第九讲01-音频(本地音乐播放,暂停,继续播放,获取播放时间,快进到指定位置,变速播放,播放data/data/目录下的音频文件,播放网络歌曲)

    Android 入门第九讲01-音频(本地音乐播放,暂停,继续播放,获取播放时间,快进到指定位置,变速播放,播放data/data/目录下的音频文件,播放网络歌曲) 准备 1.储存在raw文件夹 2. ...

  9. 如何制作自己想要的AAC音频文件

    本文只是介绍我制作AAC音频文件的整个过程,只作为参考,大家如果有更好的方法,可以不使用此方法. 1.在微信小程序里搜索 语音朗读助手,并点击打开 2.打开小程序后,点击输入文件或链接,将你需要转换成 ...

最新文章

  1. 美多后台管理和项目环境搭建
  2. 掌握 ASP.NET 之路:自定义实体类简介
  3. ggplot01:R语言坐标轴离散、连续与图例离散连续的区分
  4. python set_Python Set联合
  5. Android 手机抓包工具 Packet Capture
  6. photoshop 插件_Photoshop的光度模式
  7. C8T6和指南者使用寄存器点灯
  8. tableau数据汇总/明细/分-总的行列展示— Lee桑的学习笔记
  9. Mina中的zkApp交易snark
  10. 佐治亚理工计算机科学,佐治亚理工学院计算机科学面试经验汇总
  11. 你是否每天都认真洗手了?数据告诉你洗手时最容易忽视的部位有哪些
  12. 在win10查看本机linux的文件,在Windows 10中本机使用Linux的技巧
  13. android 安全加固总结报告,Android应用本地代码的安全加固及安全性评估
  14. 关于unittest的介绍及应用
  15. 时空猎人无尽之塔初级玩法解析攻略
  16. 今日金融词汇--- TO G 业务,是什么
  17. 配电房轨道式智能巡视机器人_HT-TSX-600-配电房轨道机器人视频巡检系统
  18. OS + linux command / Linux Command / Linux command / linux Command
  19. 三叉戟狗血剧,你的 iPhone 曾经可以换一辆玛莎拉蒂? | 2016 影响因子
  20. jzoj1156. 【GDKOI2004】使命的召唤

热门文章

  1. 纪念一下我那块分区表坏了的60G硬盘
  2. 类型[com.entity.Student]上找不到属性[StuId]
  3. js操作Cookie,实现历史浏览记录
  4. 拿去打包上线!一套代码实现1对1 、1对N在线课堂与低延迟大班课
  5. 一类形容词(形容词)和二类形容词(形容动词)的区别
  6. iOS开发面试知识整理 – OC基础 (二)
  7. word中文分词 一
  8. Type-c 充电听歌二合一转接器方案
  9. SpringBoot 整合 数据库连接池(Druid、HicariCP、C3P0等等)
  10. 圆形比例分布图怎么做_使用PPT制作环形比例图的方法