播放


在上一篇文章中,我们叙述了直播技术的环境配置(包括服务端nginx,nginx-rtmp-module, ffmpeg, Android编译,iOS编译)。从本文开始,我们将叙述播放相关的东西,播放是直播技术中关键的一步,它包括很多技术如:解码,缩放,时间基线选择,缓存队列,画面渲染,声音播放等等。我将分为三个部分为大家讲述整个播放流程;

  • Android

    第一部分是基于NativeWindow的视频渲染,主要使用的OpenGL ES2通过传入surface来将视频数据渲染到surface上显示出来。第二部分是基于OpenSL ES来音频播放。第三部分,音视频同步。我们使用的都是android原生自带的一些库来做音视频渲染处理。

  • IOS

    同样IOS也分成三个部分,第一部分视频渲染:使用OpenGLES.framework,通过OpenGL来渲染视频画面,第二部分是音频播放,基于AudioToolbox.framework做音频播放;第三部分,视音频同步。

利用原生库可以减少资源的利用,降低内存,提高性能;一般而言,如果不是通晓android、ios的程序员会选择一个统一的视频显示和音频播放库(SDL),这个库可以实现视频显示和音频播。但是增加额外的库意味着资源的浪费和性能的降低。

Android

我们首先带来android端的视频播放功能,我们分成三个部分,1、视频渲染;2、音频播放;3、时间基线(音视频同步)来阐述。

1、视频渲染


ffmpeg为我们提供浏览丰富的编解码类型(ffmpeg所具备编解码能力都是软件编解码,不是指硬件编解码。具体之后文章会详细介绍ffmpeg),视频解码包括flv, mpeg, mov 等;音频包括aac, mp3等。对于整个播放,FFmpeg主要处理流程如下:

<code class="language-C++ hljs scss has-numbering">    <span class="hljs-function">av_register_all()</span>;  <span class="hljs-comment">// 注册所有的文件格式和编解码器的库,打开的合适格式的文件上会自动选择相应的编解码库</span><span class="hljs-function">avformat_network_init()</span>; <span class="hljs-comment">// 注册网络服务</span><span class="hljs-function">avformat_alloc_context()</span>; <span class="hljs-comment">//  分配FormatContext内存,</span><span class="hljs-function">avformat_open_input()</span>;  <span class="hljs-comment">// 打开输入流,获取头部信息,配合av_close_input_file()关闭流</span><span class="hljs-function">avformat_find_stream_info()</span>; <span class="hljs-comment">// 读取packets,来获取流信息,并在pFormatCtx->streams 填充上正确的信息</span><span class="hljs-function">avcodec_find_decoder()</span>;  <span class="hljs-comment">// 获取解码器,</span><span class="hljs-function">avcodec_open2()</span>; <span class="hljs-comment">// 通过AVCodec来初始化AVCodecContext</span><span class="hljs-function">av_read_frame()</span>; <span class="hljs-comment">// 读取每一帧</span><span class="hljs-function">avcodec_decode_video2()</span>; <span class="hljs-comment">// 解码帧数据</span><span class="hljs-function">avcodec_close()</span>;  <span class="hljs-comment">// 关闭编辑器上下文</span><span class="hljs-function">avformat_close_input()</span>; <span class="hljs-comment">// 关闭文件流</span></code>

我们先来看一段代码:

<code class="language-C++ hljs php has-numbering">av_register_all();
avformat_network_init();
pFormatCtx = avformat_alloc_context();
<span class="hljs-keyword">if</span> (avformat_open_input(&pFormatCtx, pathStr, <span class="hljs-keyword">NULL</span>, <span class="hljs-keyword">NULL</span>) != <span class="hljs-number">0</span>) {LOGE(<span class="hljs-string">"Couldn't open file: %s\n"</span>, pathStr);<span class="hljs-keyword">return</span>;
}<span class="hljs-keyword">if</span> (avformat_find_stream_info(pFormatCtx, &dictionary) < <span class="hljs-number">0</span>) {LOGE(<span class="hljs-string">"Couldn't find stream information."</span>);<span class="hljs-keyword">return</span>;
}
av_dump_format(pFormatCtx, <span class="hljs-number">0</span>, pathStr, <span class="hljs-number">0</span>);
</code>

这段代码可以算是初始化FFmpeg,首先注册编解码库,为FormatContext分配内存,调用avformat_open_input打开输入流,获取头部信息,配合avformat_find_stream_info来填充FormatContext中相关内容,av_dump_format这个是dump出流信息。这个信息是这个样子的:

<code class="language-text hljs lasso has-numbering">video infomation:
Input <span class="hljs-variable">#0</span>, flv, from <span class="hljs-string">'rtmp:127.0.0.1:1935/live/steam'</span>:Metadata:Server          : NGINX RTMP (github<span class="hljs-built_in">.</span>com/sergey<span class="hljs-attribute">-dryabzhinsky</span>/nginx<span class="hljs-attribute">-rtmp</span><span class="hljs-attribute">-module</span>)displayWidth    : <span class="hljs-number">320</span>displayHeight   : <span class="hljs-number">240</span>fps             : <span class="hljs-number">15</span>profile         : level           : <span class="hljs-built_in">Duration</span>: <span class="hljs-number">00</span>:<span class="hljs-number">00</span>:<span class="hljs-number">00.00</span>, start: <span class="hljs-number">15.400000</span>, bitrate: N/AStream <span class="hljs-variable">#0</span>:<span class="hljs-number">0</span>: Video: flv1 (flv), yuv420p, <span class="hljs-number">320</span>x240, <span class="hljs-number">15</span> tbr, <span class="hljs-number">1</span>k tbn, <span class="hljs-number">1</span>k tbcStream <span class="hljs-variable">#0</span>:<span class="hljs-number">1</span>: Audio: mp3, <span class="hljs-number">11025</span> Hz, stereo, s16p, <span class="hljs-number">32</span> kb/s</code>

整个音频播放流畅其实看起来也是很简单的,主要分:1、创建实现播放引擎;2、创建实现混音器;3、设置缓冲和pcm格式;4、创建实现播放器;5、获取音频播放器接口;6、获取缓冲buffer;7、注册播放回调;8、获取音效接口;9、获取音量接口;10、获取播放状态接口;
做完这10步,整个音频播放器引擎就创建完毕,接下来就是引擎读取数据播放。

<code class="language-C++ hljs objectivec has-numbering"><span class="hljs-keyword">void</span> playBuffer(<span class="hljs-keyword">void</span> *pBuffer, <span class="hljs-keyword">int</span> size) {<span class="hljs-comment">// 判断数据可用性</span><span class="hljs-keyword">if</span> (pBuffer == <span class="hljs-literal">NULL</span> || size == -<span class="hljs-number">1</span>) {<span class="hljs-keyword">return</span>;}LOGV(<span class="hljs-string">"PlayBuff!"</span>);<span class="hljs-comment">// 数据存放进bqPlayerBufferQueue中</span>SLresult result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue,pBuffer, size);<span class="hljs-keyword">if</span> (result != SL_RESULT_SUCCESS)LOGE(<span class="hljs-string">"Play buffer error!"</span>);
}</code>

这段代码主要阐述的播放的过程,通过将数据放进bqPlayerBufferQueue,供播放引擎读取播放。记得我们在创建缓冲buffer的时候,注册了一个callback,这个callBack的作用就是通知可以向缓冲队列中添加数据,这个callBack的原型如下:

<code class="hljs lasso has-numbering"><span class="hljs-literal">void</span> videoPlayCallBack(SLAndroidSimpleBufferQueueItf bq, <span class="hljs-literal">void</span> <span class="hljs-subst">*</span>context) {<span class="hljs-comment">// 添加数据到bqPlayerBufferQueue中,通过调用playBuffer方法。</span><span class="hljs-literal">void</span><span class="hljs-subst">*</span> <span class="hljs-built_in">data</span> <span class="hljs-subst">=</span> getData();int size <span class="hljs-subst">=</span> getDataSize();playBuffer(<span class="hljs-built_in">data</span>, size);
}</code>
<code class="hljs cpp has-numbering"><span class="hljs-keyword">typedef</span> <span class="hljs-keyword">struct</span> PlayInstance {ANativeWindow *window; <span class="hljs-comment">// nativeWindow // 通过传入surface构建</span><span class="hljs-keyword">int</span> display_width; <span class="hljs-comment">// 显示宽度</span><span class="hljs-keyword">int</span> display_height; <span class="hljs-comment">// 显示高度</span><span class="hljs-keyword">int</span> stop;  <span class="hljs-comment">// 停止</span><span class="hljs-keyword">int</span> timeout_flag; <span class="hljs-comment">// 超时标记</span><span class="hljs-keyword">int</span> disable_video; VideoState *videoState; <span class="hljs-comment">//队列</span><span class="hljs-keyword">struct</span> ThreadQueue *<span class="hljs-built_in">queue</span>; <span class="hljs-comment">// 音视频帧队列</span><span class="hljs-keyword">struct</span> ThreadQueue *video_queue; <span class="hljs-comment">// 视频帧队列</span><span class="hljs-keyword">struct</span> ThreadQueue *audio_queue; <span class="hljs-comment">// 音频帧队列</span>} PlayInstance;</code>

我们主要分析延时同步的那一段代码:

<code class="hljs autohotkey has-numbering">// 延时同步int64_t pkt_pts = pavpacket.pts<span class="hljs-comment">;</span>double show_time = pkt_pts * (playInstance->videoState->video_time_base)<span class="hljs-comment">;</span>int64_t show_time_micro = show_time * <span class="hljs-number">1000000</span><span class="hljs-comment">;</span>int64_t played_time = av_gettime() - playInstance->videoState->video_start_time<span class="hljs-comment">;</span>int64_t delt<span class="hljs-built_in">a_time</span> = show_time_micro - played_time<span class="hljs-comment">;</span><span class="hljs-keyword">if</span> (delt<span class="hljs-built_in">a_time</span> < -(<span class="hljs-number">0.2</span> * <span class="hljs-number">1000000</span>)) {LOGE(<span class="hljs-string">"视频跳帧\n"</span>)<span class="hljs-comment">;</span><span class="hljs-keyword">continue</span>;} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (delt<span class="hljs-built_in">a_time</span> > <span class="hljs-number">0.2</span> * <span class="hljs-number">1000000</span>) {av_usleep(delt<span class="hljs-built_in">a_time</span>)<span class="hljs-comment">;</span>}</code>

这是一段Swift代码。在ios采用的是swift+oc+c++混合编译,正好借此熟悉swift于oc和c++的交互。enableAudio主要是创建一个audioManager实例,进行注册回调,和开始播放和暂停服务。audioManager是一个单例。是一个封装AudioToolbox类。下面的代码是激活AudioSession(初始化Audio)和失效AudioSession代码。

<code class="language-oc hljs objectivec has-numbering">- (<span class="hljs-built_in">BOOL</span>) activateAudioSession
{<span class="hljs-keyword">if</span> (!_activated) {<span class="hljs-keyword">if</span> (!_initialized) {<span class="hljs-keyword">if</span> (checkError(AudioSessionInitialize(<span class="hljs-literal">NULL</span>,kCFRunLoopDefaultMode,sessionInterruptionListener,(__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>)),<span class="hljs-string">"Couldn't initialize audio session"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;_initialized = <span class="hljs-literal">YES</span>;}<span class="hljs-keyword">if</span> ([<span class="hljs-keyword">self</span> checkAudioRoute] &&[<span class="hljs-keyword">self</span> setupAudio]) {_activated = <span class="hljs-literal">YES</span>;}}<span class="hljs-keyword">return</span> _activated;
}- (<span class="hljs-keyword">void</span>) deactivateAudioSession
{<span class="hljs-keyword">if</span> (_activated) {[<span class="hljs-keyword">self</span> pause];checkError(AudioUnitUninitialize(_audioUnit),<span class="hljs-string">"Couldn't uninitialize the audio unit"</span>);<span class="hljs-comment">/*fails with error (-10851) ? checkError(AudioUnitSetProperty(_audioUnit,kAudioUnitProperty_SetRenderCallback,kAudioUnitScope_Input,0,NULL,0),"Couldn't clear the render callback on the audio unit");*/</span>checkError(AudioComponentInstanceDispose(_audioUnit),<span class="hljs-string">"Couldn't dispose the output audio unit"</span>);checkError(AudioSessionSetActive(<span class="hljs-literal">NO</span>),<span class="hljs-string">"Couldn't deactivate the audio session"</span>);        checkError(AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_AudioRouteChange,sessionPropertyListener,(__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>)),<span class="hljs-string">"Couldn't remove audio session property listener"</span>);checkError(AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_CurrentHardwareOutputVolume,sessionPropertyListener,(__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>)),<span class="hljs-string">"Couldn't remove audio session property listener"</span>);_activated = <span class="hljs-literal">NO</span>;}
}- (<span class="hljs-built_in">BOOL</span>) setupAudio
{<span class="hljs-comment">// --- Audio Session Setup ---</span>UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;<span class="hljs-comment">//UInt32 sessionCategory = kAudioSessionCategory_PlayAndRecord;</span><span class="hljs-keyword">if</span> (checkError(AudioSessionSetProperty(kAudioSessionProperty_AudioCategory,<span class="hljs-keyword">sizeof</span>(sessionCategory),&sessionCategory),<span class="hljs-string">"Couldn't set audio category"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;<span class="hljs-keyword">if</span> (checkError(AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange,sessionPropertyListener,(__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>)),<span class="hljs-string">"Couldn't add audio session property listener"</span>)){<span class="hljs-comment">// just warning</span>}<span class="hljs-keyword">if</span> (checkError(AudioSessionAddPropertyListener(kAudioSessionProperty_CurrentHardwareOutputVolume,sessionPropertyListener,(__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>)),<span class="hljs-string">"Couldn't add audio session property listener"</span>)){<span class="hljs-comment">// just warning</span>}<span class="hljs-comment">// Set the buffer size, this will affect the number of samples that get rendered every time the audio callback is fired</span><span class="hljs-comment">// A small number will get you lower latency audio, but will make your processor work harder</span><span class="hljs-preprocessor">#if !TARGET_IPHONE_SIMULATOR</span>Float32 preferredBufferSize = <span class="hljs-number">0.0232</span>;<span class="hljs-keyword">if</span> (checkError(AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration,<span class="hljs-keyword">sizeof</span>(preferredBufferSize),&preferredBufferSize),<span class="hljs-string">"Couldn't set the preferred buffer duration"</span>)) {<span class="hljs-comment">// just warning</span>}
<span class="hljs-preprocessor">#endif</span><span class="hljs-keyword">if</span> (checkError(AudioSessionSetActive(<span class="hljs-literal">YES</span>),<span class="hljs-string">"Couldn't activate the audio session"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;[<span class="hljs-keyword">self</span> checkSessionProperties];<span class="hljs-comment">// ----- Audio Unit Setup -----</span><span class="hljs-comment">// Describe the output unit.</span>AudioComponentDescription description = {<span class="hljs-number">0</span>};description<span class="hljs-variable">.componentType</span> = kAudioUnitType_Output;description<span class="hljs-variable">.componentSubType</span> = kAudioUnitSubType_RemoteIO;description<span class="hljs-variable">.componentManufacturer</span> = kAudioUnitManufacturer_Apple;<span class="hljs-comment">// Get component</span>AudioComponent component = AudioComponentFindNext(<span class="hljs-literal">NULL</span>, &description);<span class="hljs-keyword">if</span> (checkError(AudioComponentInstanceNew(component, &_audioUnit),<span class="hljs-string">"Couldn't create the output audio unit"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;UInt32 size;<span class="hljs-comment">// Check the output stream format</span>size = <span class="hljs-keyword">sizeof</span>(AudioStreamBasicDescription);<span class="hljs-keyword">if</span> (checkError(AudioUnitGetProperty(_audioUnit,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,<span class="hljs-number">0</span>,&_outputFormat,&size),<span class="hljs-string">"Couldn't get the hardware output stream format"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;_outputFormat<span class="hljs-variable">.mSampleRate</span> = _samplingRate;<span class="hljs-keyword">if</span> (checkError(AudioUnitSetProperty(_audioUnit,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,<span class="hljs-number">0</span>,&_outputFormat,size),<span class="hljs-string">"Couldn't set the hardware output stream format"</span>)) {<span class="hljs-comment">// just warning</span>}_numBytesPerSample = _outputFormat<span class="hljs-variable">.mBitsPerChannel</span> / <span class="hljs-number">8</span>;_numOutputChannels = _outputFormat<span class="hljs-variable">.mChannelsPerFrame</span>;LoggerAudio(<span class="hljs-number">2</span>, @<span class="hljs-string">"Current output bytes per sample: %ld"</span>, _numBytesPerSample);LoggerAudio(<span class="hljs-number">2</span>, @<span class="hljs-string">"Current output num channels: %ld"</span>, _numOutputChannels);<span class="hljs-comment">// Slap a render callback on the unit</span>AURenderCallbackStruct callbackStruct;callbackStruct<span class="hljs-variable">.inputProc</span> = renderCallback; <span class="hljs-comment">// 注册回调,这个回调是用来取数据的,也就是</span>callbackStruct<span class="hljs-variable">.inputProcRefCon</span> = (__bridge <span class="hljs-keyword">void</span> *)(<span class="hljs-keyword">self</span>);<span class="hljs-keyword">if</span> (checkError(AudioUnitSetProperty(_audioUnit,kAudioUnitProperty_SetRenderCallback,kAudioUnitScope_Input,<span class="hljs-number">0</span>,&callbackStruct,<span class="hljs-keyword">sizeof</span>(callbackStruct)),<span class="hljs-string">"Couldn't set the render callback on the audio unit"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;<span class="hljs-keyword">if</span> (checkError(AudioUnitInitialize(_audioUnit),<span class="hljs-string">"Couldn't initialize the audio unit"</span>))<span class="hljs-keyword">return</span> <span class="hljs-literal">NO</span>;<span class="hljs-keyword">return</span> <span class="hljs-literal">YES</span>;
}</code>

总结


本文主要是讲述了ffmpeg实现播放的逻辑,分为android和ios两端,根据两端平台的特性做了相应的处理。在android端采用的是NativeWindow(surface)实现视频播放,OpenSL ES实现音频播放。实现音视频同步的逻辑是基于第三方时间基准线,音频和视频同时调整的方案。在ios端采用的是OpenGL实现视频渲染,AudioToolbox实现音频播放。音视频同步和android采用的是一样。其中两端的ffmpeg逻辑是一致的。在ios端OpenGL实现视频渲染没有重点阐述如何使用OpenGL。这个有兴趣的同学可以自行研究。
备注:整个代码工程等整理之后会发布出来。
最后添加两张播放效果图

直播技术(从服务端到客户端)二相关推荐

  1. 在laravel5.8中集成swoole组件----用协程实现的服务端和客户端(二)---静态文件如何部署...

    目前,较为成熟的技术是采用laravelS组件,注意和laravel 区别laravelS多了一个大写的S,由于laravelS默认监听5200端口,所以laravel项目要做一些调整 例如: 静态文 ...

  2. 直播技术(从服务端到客户端)一

    环境部署 2015年开始直播变得越来越流行,很多的直播平台也应运而生,直播是一个很有技术的项目,从服务端到客户端到web等等.我们将写一序列的博客来阐述直播中的技术,这包括服务端技术和客户端技术.包括 ...

  3. 成品app直播源码,服务端与客户端传输视频文件

    成品app直播源码,服务端与客户端传输视频文件相关的代码 Server端 #define WIN32_LEAN_AND_MEAN #define _WINSOCK_DEPRECATED_NO_WARN ...

  4. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  5. 搭建简易的物联网服务端和客户端-Maibu控制(二十一)

    创建麦布应用程序,麦步按键控制.原理和网页控制差不多,就是麦步访问之前创建的两个buttonclick接口.感谢qs100371大神. 代码地址:https://github.com/ZZES-ZVD ...

  6. 【★更新★】高性能 Windows Socket 服务端与客户端组件(HP-Socket v2.0.1 源代码及测试用例下载)...

    HP-Socket 以前为某大型通信项目开发了一套通用 Windows Socket TCP 底层通信组件,组件代号为 HP-Socket.现在把 HP-Socket 的所有代码向大众公开,希望能对大 ...

  7. 使用HTML5的WebSocket实现服务端和客户端数据通信(有演示和源码)

    WebSocket协议是基于TCP的一种新的网络协议.WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术.依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信. ...

  8. Java的oauth2.0 服务端与客户端的实现

    oauth原理简述 oauth本身不是技术,而是一项资源授权协议,重点是协议!Apache基金会提供了针对Java的oauth封装.我们做Java web项目想要实现oauth协议进行资源授权访问,直 ...

  9. 服务端向客户端主动发送消息

    通常情况下,无论是web浏览器还是移动app,我们与服务器之间的交互都是主动的,客户端向服务器端发出请求,然后服务器端返回数据给客户端,客户端浏览器再将信息呈现,客户端与服务端对应的模式是: 客户端请 ...

  10. 破甲两千六 Spring Cloud 教程(三):添加Spring Cloud 的 Netflix Eureka 插件,实现服务端、客户端的发现与注册

    写在前面: Spring Cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理.服务发现.断路器.路由.微代理.事件总线.全局锁.决策竞选.分布式会话等等. 5大常用组件: 服务发现 ...

最新文章

  1. Maven - Dynamic Web Module 3.0 requires Java 1.6 or newer.
  2. 解决“Internet Explorer 无法打开 Internet站点已终止操作”问题(转)
  3. C++ 继承和派生 及 学生管理范例
  4. 代码控制UI,View
  5. 量子物理 詹班 计算机,6量子物理作业答案
  6. UA MATH571A 一元线性回归I 模型设定与估计
  7. [JavaWeb-MySQL]DDL_操作数据库,表
  8. 双极型三极管共集电极、共基极放大电路
  9. C#中 标识符“XXX”不符合 CLS
  10. 2019-11-29GPS干扰技术解析
  11. Silvaco TCAD仿真3——DeckBuild
  12. Linux升级ilo,利用HP iLO4安装系统
  13. [BBC纪录片][2009][自然界最惊异的事件][Nature's.Most.Amazing.Events][中英字幕][蓝光720P高清][全6集][17.77GB]
  14. DNX 版本升级命令
  15. Winform使用DSO Framer控件嵌入office 异常总结及解决方法
  16. 内网搭建maven私库
  17. java 俄罗斯方块消除整行_帮忙看下我的俄罗斯方块满行删除方法,为嘛一次只能删除1行。。...
  18. 两电平变流器matlab仿真,基于H桥级联型五电平逆变器Matlab仿真分析.doc
  19. 【解决方案】解决ImportError: Library “GLU“ not found.问题
  20. 正运动技术荣膺“CMCD 2020年度运动控制领域最具成长品牌”等三项大奖

热门文章

  1. (转)Symbian启动J2ME程序
  2. 利用opencv中的类FileStorage生成和读取XML和YAML文件
  3. 虚拟化方案应用场景及优劣
  4. Loj 6485. LJJ 学二项式定理
  5. NOIP普及组第1题(1995-2018)
  6. hbase RowFilter如何根据rowkey查询以及实例实现代码
  7. 第一次react-native项目实践要点总结
  8. Go 语言 bytes.buffer write 相关操作
  9. 当医疗健康加上大数据,会碰撞出什么火花?
  10. 【EntityFramework Core】实体实例化注入