原文地址::https://www.jianshu.com/p/73b0a0a9bb0d

相关文章

1、FFmpeg音频解码播放----https://www.jianshu.com/p/76562aba84fb

2、通过FFMpeg播放音乐文件----https://blog.csdn.net/chenhy24/article/details/84201421

3、ffmpeg命令操作音频格式转换----https://www.bbsmax.com/A/mo5kbyeEJw/

4、使用FFMpeg 解码音频文件----https://blog.csdn.net/douzhq/article/details/82937422

5、FFmpeg的音频处理详解----https://blog.csdn.net/fireroll/article/details/83032025

6、FFmpeg 入门(3):播放音频----https://blog.csdn.net/naibei/article/details/81086483

7、ffmpeg 音视频文件播放模块----https://blog.csdn.net/wer85121430/article/details/79689002

8、FFmpeg简易播放器的实现-音频播放----https://blog.csdn.net/leisure_chn/article/details/87641899

9、最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)----https://blog.csdn.net/leixiaohua1020/article/details/38868499

10、[总结]视音频编解码技术零基础学习方法----https://blog.csdn.net/leixiaohua1020/article/details/18893769

FFmpeg音频播放器(1)-简介
FFmpeg音频播放器(2)-编译动态库
FFmpeg音频播放器(3)-将FFmpeg加入到Android中
FFmpeg音频播放器(4)-将mp3解码成pcm
FFmpeg音频播放器(5)-单输入filter(volume,atempo)
FFmpeg音频播放器(6)-多输入filter(amix)
FFmpeg音频播放器(7)-使用OpenSLES播放音频
FFmpeg音频播放器(8)-创建FFmpeg播放器
FFmpeg音频播放器(9)-播放控制
有了前面一系列的准备知识,可以开始打造FFmpeg音频播放器了。主要需求,多个音频混音播放,每个音轨音量控制,合成音频变速播放。而音频的暂停,进度条,停止放到下一节讲述。

AudioPlayer类

首先我们创建一个C++ Class名为AudioPlayer,为了能够实现音频解码,过滤,播放功能,我们需要解码、过滤、队列、输出pcm相关、多线程、Open SL ES相关的成员变量,代码如下:

//解码
int fileCount;                  //输入音频文件数量
AVFormatContext **fmt_ctx_arr;  //FFmpeg上下文数组
AVCodecContext **codec_ctx_arr; //解码器上下文数组
int *stream_index_arr;          //音频流索引数组
//过滤
AVFilterGraph *graph;
AVFilterContext **srcs;         //输入filter
AVFilterContext *sink;          //输出filter
char **volumes;                 //各个音频的音量
char *tempo;                    //播放速度0.5~2.0//AVFrame队列
std::vector<AVFrame *> queue;   //队列,用于保存解码过滤后的AVFrame//输入输出格式
SwrContext *swr_ctx;            //重采样,用于将AVFrame转成pcm数据
uint64_t in_ch_layout;
int in_sample_rate;            //采样率
int in_ch_layout_nb;           //输入声道数,配合swr_ctx使用
enum AVSampleFormat in_sample_fmt; //输入音频采样格式uint64_t out_ch_layout;
int out_sample_rate;            //采样率
int out_ch_layout_nb;           //输出声道数,配合swr_ctx使用
int max_audio_frame_size;       //最大缓冲数据大小
enum AVSampleFormat out_sample_fmt; //输出音频采样格式// 进度相关
AVRational time_base;           //刻度,用于计算进度
double total_time;              //总时长(秒)
double current_time;            //当前进度
int isPlay = 0;                 //播放状态1:播放中//多线程
pthread_t decodeId;             //解码线程id
pthread_t playId;               //播放线程id
pthread_mutex_t mutex;          //同步锁
pthread_cond_t not_full;        //不为满条件,生产AVFrame时使用
pthread_cond_t not_empty;       //不为空条件,消费AVFrame时使用//Open SL ES
SLObjectItf engineObject;       //引擎对象
SLEngineItf engineItf;          //引擎接口
SLObjectItf mixObject;          //输出混音对象
SLObjectItf playerObject;       //播放器对象
SLPlayItf playItf;              //播放器接口
SLAndroidSimpleBufferQueueItf bufferQueueItf;   //缓冲接口

解码播放流程

整个音频处理播放流程如上图,首先,我们需要两个线程一个是解码线程,一个是播放线程。解码线程负责多个音频文件的解码,过滤,加入队列操作,播放线程则需要从队列中取出处理后的AVFrame,然后转成pcm输入,通过缓冲回调播放音频。
为了初始化这些成员变量,我们按照每块成员列表定义了对于的初始化方法。

int createPlayer();                     //创建播放器
int initCodecs(char **pathArr);         //初始化解码器
int initSwrContext();                   //初始化SwrContext
int initFilters();                      //初始化过滤器

而在构造函数中传入音频文件数组,和文件数量,初始化相关方法

AudioPlayer::AudioPlayer(char **pathArr, int len) {//初始化fileCount = len;//默认音量1.0 速度1.0volumes = (char **) malloc(fileCount * sizeof(char *));for (int i = 0; i < fileCount; i++) {volumes[i] = "1.0";}tempo = "1.0";pthread_mutex_init(&mutex, NULL);pthread_cond_init(&not_full, NULL);pthread_cond_init(&not_empty, NULL);initCodecs(pathArr);avfilter_register_all();initSwrContext();initFilters();createPlayer();
}

这里我们还初始化了控制各个音频音量和速度的变量,同步锁和条件变量(生产消费控制)。

初始化解码器数组

int AudioPlayer::initCodecs(char **pathArr) {LOGI("init codecs");av_register_all();fmt_ctx_arr = (AVFormatContext **) malloc(fileCount * sizeof(AVFormatContext *));codec_ctx_arr = (AVCodecContext **) malloc(fileCount * sizeof(AVCodecContext *));stream_index_arr = (int *) malloc(fileCount * sizeof(int));for (int n = 0; n < fileCount; n++) {AVFormatContext *fmt_ctx = avformat_alloc_context();fmt_ctx_arr[n] = fmt_ctx;const char *path = pathArr[n];if (avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0) {//打开文件LOGE("could not open file:%s", path);return -1;}if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {//读取音频格式文件信息LOGE("find stream info error");return -1;}//获取音频索引int audio_stream_index = -1;for (int i = 0; i < fmt_ctx->nb_streams; i++) {if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {audio_stream_index = i;LOGI("find audio stream index:%d", audio_stream_index);break;}}if (audio_stream_index < 0) {LOGE("error find stream index");return -1;}stream_index_arr[n] = audio_stream_index;//获取解码器AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);codec_ctx_arr[n] = codec_ctx;AVStream *stream = fmt_ctx->streams[audio_stream_index];avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar);AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);if (n == 0) {//获取输入格式in_sample_fmt = codec_ctx->sample_fmt;in_ch_layout = codec_ctx->channel_layout;in_sample_rate = codec_ctx->sample_rate;in_ch_layout_nb = av_get_channel_layout_nb_channels(in_ch_layout);max_audio_frame_size = in_sample_rate * in_ch_layout_nb;time_base = fmt_ctx->streams[audio_stream_index]->time_base;int64_t duration = stream->duration;total_time = av_q2d(stream->time_base) * duration;LOGI("total time:%lf", total_time);} else {//如果是多个文件,判断格式是否一致(采用率,格式、声道数)if (in_ch_layout != codec_ctx->channel_layout|| in_sample_fmt != codec_ctx->sample_fmt|| in_sample_rate != codec_ctx->sample_rate) {LOGE("输入文件格式相同");return -1;}}//打开解码器if (avcodec_open2(codec_ctx, codec, NULL) < 0) {LOGE("could not open codec");return -1;}}return 1;
}

这里将输入音频的格式信息保存起来,用于SwrContext初始化、Filter初始化。

初始化Filters

int AudioPlayer::initFilters() {LOGI("init filters");graph = avfilter_graph_alloc();srcs = (AVFilterContext **) malloc(fileCount * sizeof(AVFilterContext **));char args[128];AVDictionary *dic = NULL;//混音过滤器AVFilter *amix = avfilter_get_by_name("amix");AVFilterContext *amix_ctx = avfilter_graph_alloc_filter(graph, amix, "amix");snprintf(args, sizeof(args), "inputs=%d:duration=first:dropout_transition=3", fileCount);if (avfilter_init_str(amix_ctx, args) < 0) {LOGE("error init amix filter");return -1;}const char *sample_fmt = av_get_sample_fmt_name(in_sample_fmt);snprintf(args, sizeof(args), "sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,in_sample_rate, sample_fmt, in_ch_layout);for (int i = 0; i < fileCount; i++) {AVFilter *abuffer = avfilter_get_by_name("abuffer");char name[50];snprintf(name, sizeof(name), "src%d", i);srcs[i] = avfilter_graph_alloc_filter(graph, abuffer, name);if (avfilter_init_str(srcs[i], args) < 0) {LOGE("error init abuffer filter");return -1;}//音量过滤器AVFilter *volume = avfilter_get_by_name("volume");AVFilterContext *volume_ctx = avfilter_graph_alloc_filter(graph, volume, "volume");av_dict_set(&dic, "volume", volumes[i], 0);if (avfilter_init_dict(volume_ctx, &dic) < 0) {LOGE("error init volume filter");return -1;}//将输入端链接到volume过滤器if (avfilter_link(srcs[i], 0, volume_ctx, 0) < 0) {LOGE("error link to volume filter");return -1;}//链接到混音amix过滤器if (avfilter_link(volume_ctx, 0, amix_ctx, i) < 0) {LOGE("error link to amix filter");return -1;}av_dict_free(&dic);}//变速过滤器atempoAVFilter *atempo = avfilter_get_by_name("atempo");AVFilterContext *atempo_ctx = avfilter_graph_alloc_filter(graph, atempo, "atempo");av_dict_set(&dic, "tempo", tempo, 0);if (avfilter_init_dict(atempo_ctx, &dic) < 0) {LOGE("error init atempo filter");return -1;}//输出格式AVFilter *aformat = avfilter_get_by_name("aformat");AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");snprintf(args, sizeof(args), "sample_rates=%d:sample_fmts=%s:channel_layouts=0x%" PRIx64,in_sample_rate, sample_fmt, in_ch_layout);if (avfilter_init_str(aformat_ctx, args) < 0) {LOGE("error init aformat filter");return -1;}//输出缓冲AVFilter *abuffersink = avfilter_get_by_name("abuffersink");sink = avfilter_graph_alloc_filter(graph, abuffersink, "sink");if (avfilter_init_str(sink, NULL) < 0) {LOGE("error init abuffersink filter");return -1;}//将amix链接到atempoif (avfilter_link(amix_ctx, 0, atempo_ctx, 0) < 0) {LOGE("error link to atempo filter");return -1;}if (avfilter_link(atempo_ctx, 0, aformat_ctx, 0) < 0) {LOGE("error link to aformat filter");return -1;}if (avfilter_link(aformat_ctx, 0, sink, 0) < 0) {LOGE("error link to abuffersink filter");return -1;}if (avfilter_graph_config(graph, NULL) < 0) {LOGE("error config graph");return -1;}return 1;
}

通过初始化解码器获取的输入音频格式信息,可以初始化abuffer输入filter(采样率、格式、声道必须匹配),然后可以链接volume ,amix,atempo filter。这样音频就可以实现调音,混音,变速的效果。

初始化SwrContext

int AudioPlayer::initSwrContext() {LOGI("init swr context");swr_ctx = swr_alloc();out_sample_fmt = AV_SAMPLE_FMT_S16;out_ch_layout = AV_CH_LAYOUT_STEREO;out_ch_layout_nb = 2;out_sample_rate = in_sample_rate;max_audio_frame_size = out_sample_rate * 2;swr_alloc_set_opts(swr_ctx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout,in_sample_fmt, in_sample_rate, 0, NULL);if (swr_init(swr_ctx) < 0) {LOGE("error init SwrContext");return -1;}return 1;
}

为了能使得解码出来的AVFrame能在OpenSL ES下播放,我们将采用格式固定为16位的AV_SAMPLE_FMT_S16,声道为立体声AV_CH_LAYOUT_STEREO,声道数为2,采样率和输入一样。缓冲回调pcm数据最大值为采样率*2。

初始化OpenSL ES播放器

int AudioPlayer::createPlayer() {//创建播放器//创建并且初始化引擎对象
//    SLObjectItf engineObject;slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);//获取引擎接口
//    SLEngineItf engineItf;(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf);//通过引擎接口获取输出混音
//    SLObjectItf mixObject;(*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);(*mixObject)->Realize(mixObject, SL_BOOLEAN_FALSE);//设置播放器参数SLDataLocator_AndroidSimpleBufferQueueandroid_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};SLuint32 samplesPerSec = (SLuint32) out_sample_rate * 1000;//pcm格式SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,2,//两声道samplesPerSec,SL_PCMSAMPLEFORMAT_FIXED_16,SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//SL_BYTEORDER_LITTLEENDIAN};SLDataSource slDataSource = {&android_queue, &pcm};//输出管道SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, mixObject};SLDataSink audioSnk = {&outputMix, NULL};const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};//通过引擎接口,创建并且初始化播放器对象
//    SLObjectItf playerObject;(*engineItf)->CreateAudioPlayer(engineItf, &playerObject, &slDataSource, &audioSnk, 1, ids,req);(*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);//获取播放接口
//    SLPlayItf playItf;(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);//获取缓冲接口
//    SLAndroidSimpleBufferQueueItf bufferQueueItf;(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);//注册缓冲回调(*bufferQueueItf)->RegisterCallback(bufferQueueItf, _playCallback, this);return 1;
}

这里的pcm格式和SwrContext设置的参数要一致

启动播放线程和解码线程

void *_decodeAudio(void *args) {AudioPlayer *p = (AudioPlayer *) args;p->decodeAudio();pthread_exit(0);
}void *_play(void *args) {AudioPlayer *p = (AudioPlayer *) args;p->setPlaying();pthread_exit(0);
}void AudioPlayer::setPlaying() {//设置播放状态(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);_playCallback(bufferQueueItf, this);
}void AudioPlayer::play() {LOGI("play...");isPlay = 1;pthread_create(&decodeId, NULL, _decodeAudio, this);pthread_create(&playId, NULL, _play, this);
}

play方法中我们pthread_create启动播放和解码线程,播放线程通过播放接口设置播放中状态,然后回调缓冲接口,在回调中,取出队列中的AVFrame转成pcm,然后通过Enqueue播放。解码线程负责解码过滤出AVFrame,加入到队列中。

缓冲回调

void _playCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {AudioPlayer *player = (AudioPlayer *) context;AVFrame *frame = player->get();if (frame) {int size = av_samples_get_buffer_size(NULL, player->out_ch_layout_nb, frame->nb_samples,player->out_sample_fmt, 1);if (size > 0) {uint8_t *outBuffer = (uint8_t *) av_malloc(player->max_audio_frame_size);swr_convert(player->swr_ctx, &outBuffer, player->max_audio_frame_size,(const uint8_t **) frame->data, frame->nb_samples);(*bq)->Enqueue(bq, outBuffer, size);}}
}

解码过滤

void AudioPlayer::decodeAudio() {LOGI("start decode...");AVFrame *frame = av_frame_alloc();AVPacket *packet = av_packet_alloc();int ret, got_frame;int index = 0;while (isPlay) {LOGI("decode frame:%d", index);for (int i = 0; i < fileCount; i++) {AVFormatContext *fmt_ctx = fmt_ctx_arr[i];ret = av_read_frame(fmt_ctx, packet);if (packet->stream_index != stream_index_arr[i])continue;//不是音频packet跳过if (ret < 0) {LOGE("decode finish");goto end;}ret = avcodec_decode_audio4(codec_ctx_arr[i], frame, &got_frame, packet);if (ret < 0) {LOGE("error decode packet");goto end;}if (got_frame <= 0) {LOGE("decode error or finish");goto end;}ret = av_buffersrc_add_frame(srcs[i], frame);if (ret < 0) {LOGE("error add frame to filter");goto end;}}LOGI("time:%lld,%lld,%lld", frame->pkt_dts, frame->pts, packet->pts);while (av_buffersink_get_frame(sink, frame) >= 0) {frame->pts = packet->pts;LOGI("put frame:%d,%lld", index, frame->pts);put(frame);}index++;}end:av_packet_unref(packet);av_frame_unref(frame);
}

这里有一个注意的点是,通过av_read_frame读取的packet不一定是音频流,所以需要通过音频流索引过滤packet。在av_buffersink_get_frame获取的AVFrame中,将pts修改为packet里的pts,用于保存进度(过滤后的pts时间进度不是当前解码的进度)。

AVFrame存和取

/*** 将AVFrame加入到队列,队列长度为5时,阻塞等待* @param frame* @return*/
int AudioPlayer::put(AVFrame *frame) {AVFrame *out = av_frame_alloc();if (av_frame_ref(out, frame) < 0)return -1;//复制AVFramepthread_mutex_lock(&mutex);if (queue.size() == 5) {LOGI("queue is full,wait for put frame:%d", queue.size());pthread_cond_wait(&not_full, &mutex);}queue.push_back(out);pthread_cond_signal(&not_empty);pthread_mutex_unlock(&mutex);return 1;
}/*** 取出AVFrame,队列为空时,阻塞等待* @return*/
AVFrame *AudioPlayer::get() {AVFrame *out = av_frame_alloc();pthread_mutex_lock(&mutex);while (isPlay) {if (queue.empty()) {pthread_cond_wait(&not_empty, &mutex);} else {AVFrame *src = queue.front();if (av_frame_ref(out, src) < 0)return NULL;queue.erase(queue.begin());//删除取出的元素av_free(src);if (queue.size() < 5)pthread_cond_signal(&not_full);pthread_mutex_unlock(&mutex);current_time = av_q2d(time_base) * out->pts;LOGI("get frame:%d,time:%lf", queue.size(), current_time);return out;}}pthread_mutex_unlock(&mutex);return NULL;
}

通过两个条件变量,实现一个缓冲太小为5的生产消费模型,用于AVFrame队列的存和取。
通过以上代码就可以实现音量为1,速度为1的多音频播放了,而具体的音频控制放在下一节讲。

项目地址

播放时需要将assets的音频文件放到对应sd卡目录

https://github.com/iamyours/FFmpegAudioPlayer

作者:星星y
链接:https://www.jianshu.com/p/73b0a0a9bb0d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

FFmpeg音频播放器(8)-创建FFmpeg播放器相关推荐

  1. 最简单的基于FFMPEG+SDL的音频播放器 拆分-解码器和播放器

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  2. 基于ffmpeg和libvlc的视频剪辑、播放器

    以前研究的时候,写过一个简单的基于VLC的视频播放器.后来因为各种项目,有时为了方便测试,等各种原因,陆续加了一些功能,现在集成了视频播放.视频加减速.视频剪切,视频合并(增加中)等功能在一起.有时候 ...

  3. ffmpeg中音频解码方法(附代码)+ffmpeg音频解码播放速度快的问题(随手笔记,以供查阅)

    最近在做一款取名为变速不变调播放器的时候,解码音频遇到了些问题(ffmpeg音频解码播放速度快的问题),网络上的方法对绝大多数的音视频文件有效,但是对于某些音频会有问题,比如某些ADPCM编码的WAV ...

  4. 基于FFmpeg和SDL1.2的极简播放器实现

    思路 基于FFmpeg写一个播放器,其实十分的简单.实际上,主要是对FFmpeg的API的封装,同时,我们需要将音视频通过主机呈现出来,所以还依赖于平台的SDL库,整体步骤和思路如下: 1. 编译用于 ...

  5. ffmpeg音频播放代码示例-avcodec_decode_audio4

    一.概述 最近在学习ffmpeg解码的内容,参考了官方的教程http://dranger.com/ffmpeg/tutorial03.html,结果发现这个音频解码的教程有点问题.参考了各种博客,并同 ...

  6. 利用FFmpeg和OpenGL ES 实现 3D 全景播放器

    前言 我们已经利用 FFmpeg + OpenGLES + OpenSLES 实现了一个多媒体播放器,本文将基于此播放器实现一个酷炫的 3D 全景播放器. 全景播放器原理 全景视频是由多台摄像机在一个 ...

  7. 自定义音频播放器_创建自定义HTML5音频播放器

    自定义音频播放器 在本教程中,我将向您介绍HTML5音频,并向您展示如何创建自己的播放器. 如果您想走捷径,请查看Envato市场上可用的现成的HTML5音频播放器 . 它使您可以从各种来源创建播放列 ...

  8. srs服务器播放文件,使用SRS+ffmpeg搭建流媒体服务器播放m3u8格式视频

    1.简介 srs是一个简单的流媒体开源直播软件,ffmpeg是完整的跨平台解决方案,用于记录,转换和流传输音频和视频. 2.相关 官网下载页面:点击我到达 在线演示播放页面:点击我到达 Git页面:点 ...

  9. 从测试到开发掌握 ffmpeg安装 以及amr 转换MP3 并且播放

    1:原理 调用ffmpeg 执行转换 即:先把amr 下载到服务器 调用 ffmpeg -i 转换生成MP3 文件 通过二进制流的形式 ajax点击播放异步转换并且生成流通过audio的src属性在w ...

最新文章

  1. Java利用QRCode.jar包实现二维码编码与解码
  2. 禁用viewstate怎么还保存状态?
  3. 向腾讯云windows服务器传输文件,如何上传本地文件到腾讯云Windows服务器上?
  4. RocketMQ的安装与配置
  5. 2020CCPC(长春) - Ragdoll(启发式合并+带权并查集)
  6. c语言统计数据,数据统计
  7. JAVA中protected的作用
  8. 灰度值怎么降级_微服务生态的灰度发布如何实现?
  9. Homework1_3015218130_许鹏程
  10. wpf 依赖强制回调
  11. ArcGIS 制作林地成分栅格数据
  12. L2-006 树的遍历 (25 point(s))
  13. adb 切换usb模式_利用adb命令打开usb调试
  14. 批量修改图片的后缀名以及删除相同的符号
  15. c#如何wmf图片转换成png图片_【C#】使用fo-dicom完成BMP,JPG,PNG图片转换为DICOM文件-阿里云开发者社区...
  16. Glide 加载矩形圆角图片
  17. 【初等概率论】 02
  18. 酒仙网将上市:营销促营收增长,深陷纠纷案,部分股权被法院冻结
  19. 中秋节快乐 | 9月21日 星期二 | 天舟三号货运飞船发射成功;理想汽车下调第三季度交付量预期;凯德集团业务重组完成...
  20. Python保存图像的几种方式

热门文章

  1. j计算机实验室安全操作规范,实验室安全技术操作规范.doc
  2. 木木璐(林璐)来报到
  3. 线控转向,包含设计说明书,carsim模型,MATLAB Simulink模型全套(工程项目线上支持)
  4. windows10系统桌面图标小盾牌去除方法
  5. django + MySQL + flup + Nginx 的一些相关配置文件的备份
  6. 深圳软件测试培训:Selenium断言与验证
  7. matlab生成热敏电阻温度和阻值的数学关系式
  8. 【渝粤题库】广东开放大学 建筑制图 形成性考核
  9. 病毒分析之撒旦(Satan)勒索病毒分析解密(AES256 ECB算法)
  10. Rule of lawlessness 南非法治之战 | 经济学人中英双语对照精读笔记