实现一个wasm视频解码渲染的小demo,网页端集成emcc编译的ffmpeg库,实现视频解码,使用WebGL实现视频渲染。demo中包含了一个基于mongoose的微型Web服务器,用于网页的Web服务和视频流传输,基本无需额外搭建环境以及编译第三方库,可以简单地移植到嵌入式系统中用于网页视频播放视频。学习过程中主要参考了大神代码和文章

编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(2)使用Emscripten编译 - 腾讯云开发者社区-腾讯云

demo地址

wasm_websocket_player: wasm 解码渲染demo

1.编译

1.1 ffmpeg emcc版本编译

首先需要获取emcc用于编译,Mac下可以直接通过brew install来获取。下一步就是通过emcc,将ffmpeg编译对应的静态库。注意这里需要将ffmpeg中平台相关以及汇编相关的选项禁掉,毕竟这里最终都是在js虚拟机中执行,硬件加速相关的操作都需要去掉。下面是demo中编译ffmpeg使用的命令,源文件在demo的third_party文件下。

mkdir ffmpeg-emcc
cd FFmpeg_new
#make clean
emconfigure ./configure --cc="emcc" --cxx="em++" --ar="emar" \
--ranlib=emranlib --prefix=../ffmpeg-emcc/ \
--enable-cross-compile --target-os=none \
--arch=x86_32 --cpu=generic --enable-gpl \
--disable-avdevice  \
--disable-postproc --disable-avfilter \
--disable-programs \
--disable-everything --enable-avformat  \--enable-decoder=hevc --enable-decoder=h264 --enable-decoder=h264_qsv \--enable-decoder=hevc_qsv \--enable-decoder=aac \--disable-ffplay --disable-ffprobe  --disable-asm \--disable-doc --disable-devices --disable-network \--disable-hwaccels \--disable-debug \--enable-protocol=file --disable-indevs --disable-outdevs \--enable-parser=hevc --enable-parser=h264emmake make -j4
emmake make install

1.2 客户端源代码编译

ffmpeg静态链接库生成后,下一步就可以编译demo中客户端相关的源码,包括我们自己调用ffmpeg库的代码,c层与js层交互的代码,以及ffmpeg静态链接库,最终生成一个js文件和一个.wasm库,在网页中我们通过调用生成的js文件进行解码。下面是编译命令,源文件在demo工程的client文件下的build_with_emcc.sh。

export TOTAL_MEMORY=67108864CURR_DIR=$(pwd)
export FFMPEG_PATH=$CURR_DIR/../third_party/ffmpeg-emccemcc --bind ../common/video_decoder.cc ../common/h264_reader.cc ../common/frame_queue.cc main.cc\-std=c++11 \-s USE_PTHREADS=1\-g \-I "${FFMPEG_PATH}/include" \-L ${FFMPEG_PATH}/lib \-lavutil -lavformat -lavcodec \-s WASM=1 -Wall \-s EXPORTED_FUNCTIONS="['_malloc','_free']" \-s ASSERTIONS=0 \-s ALLOW_MEMORY_GROWTH=1 \-s TOTAL_MEMORY=167772160 \-o ${PWD}/player.js

最终会生成player.js以及player.wasm文件。

1.3 demo中server的编译与demo运行

demo中提供了一个微型Web server,提供http服务以及websocket数据传输。考虑到demo主要用于嵌入式平台,这里选择了mongoose作为Web服务器,只需要在源代码中引入一个.c文件和一个.h文件即可使用,无需复杂的编译和依赖库。demo中使用了一个本地h264文件,server收到客户端请求后会读取这个本地文件,通过avformat读取每帧h264,实际使用中可以将这块的代码更换为当前设备的采集和编码。目前调试是在Mac的arm64版本上编译,直接运行server目录下cmake即可。

可以直接在server目录下运行run.sh,即可完成客户端编译,服务端编译以及相关文件的拷贝。目前写死使用8000端口。

2.解码流程实现

2.1 js传递视频流给wasm解码

wasm内存分配与释放

这里首先介绍一下js与底层wasm的交互方式。一般视频流数据数量较小,可以直接为其分配内存空间,这里我们直接通过在js层调用_malloc和_free进行分配和释放内存,这些内存可以被wasm代码所使用。这里首先分配wasm可以使用的内存,下一步就是将js的Uint8Array数据拷贝给这块内存,这样wasm中的代码就可以操作这块内存了。

js传递数据给wasm

这里可以在C++层通过EMSCRIPTEN_BINDINGS对C++函数进行封装,基本数据类型可以使用普通的C/C++数据类型,传入js所分配的内存,在C/C++层直接使用uintptr_t类型即可。下面使用我们deocder类来进行说明。
decoder类的C++类,emscripten::val lambda类型可以将一个js函数传入wasm作为回调函数。

class StreamDecoderWrapper{
public:StreamDecoderWrapper(){}~StreamDecoderWrapper(){}void OpenAvcDecoder(emscripten::val lambda){... ...decoder.OpenWithCodecID(AV_CODEC_ID_H264);decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{... ...auto frame_wrapper = std::make_shared<VideoFrameWrapper>()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame);... ...lambda(frame_wrapper);return 0;});}void DecodeVideoPacket(uintptr_t buf_p, int size){uint8_t *data = reinterpret_cast<uint8_t *>(buf_p);... ...}void CloseDecoder(){... ...}private:... ...
};

注册StreamDecoderWrapper,让js代码可以识别这个类。这个操作类似jni的动态注册,将字符串与C++类名和方法名对应,这样在js层中可以直接使用这个字符串创建对象并调用方法。

#include <emscripten/bind.h>
#ifndef NDEBUG
#include <sanitizer/lsan_interface.h>
#endif#include "stream_decoder_wrapper.h"using namespace emscripten;EMSCRIPTEN_BINDINGS(module){... ...class_<StreamDecoderWrapper>("StreamDecoderWrapper").constructor<>().function("openAvcDecoder", &StreamDecoderWrapper::OpenAvcDecoder).function("decodeVideoPacket", &StreamDecoderWrapper::DecodeVideoPacket).function("closeDecoder",  &StreamDecoderWrapper::CloseDecoder);... ...
}

js层调用wasm类StreamDecoderWrapper,可以完全当作是一个js类,通过new创建对象并调用方法。

class StreamDecoderWrapperJS{#stream_decoder_inner = null;StreamDecoderWrapperJS(){}openAvcDecoder(frame_callback){this.#stream_decoder_inner = new Module.StreamDecoderWrapper()this.#stream_decoder_inner.openAvcDecoder((videoFrameWrapperJS)=>{... ...frame_callback(videoFrameWrapperJS)videoFrameWrapperJS.delete();})}decodeVideoPacket(data, size, headsize){... ...let data_array = new Uint8Array(data)let data_slice = data_array.slice(headsize, headsize+size)let data_len = size;let buf = _malloc(data_len);HEAPU8.set(data_slice, buf);this.#stream_decoder_inner.decodeVideoPacket(buf, data_len)_free(buf);... ...}closeDecoder(){this.#stream_decoder_inner.closeDecoder();}
}

2.2 解码

在wasm中收到js传来的buffer数据后,就可以进行下一步解码。代码如下,可以看到这里都是普通C/C++的数据类型,js层传来的buf_p在这里直接就是一个uint8_t类型的buffer,拿到正确数据交给ffmpeg进行解码即可。

void DecodeVideoPacket(uintptr_t buf_p, int size){uint8_t *data = reinterpret_cast<uint8_t *>(buf_p);if(data && (size != 0)){... ...decoder.Decode(data, size);... ...}
}

ffmpeg解码代码这里就不再赘述,还不太了解的朋友可以参考ffmpeg中doc下的例子。这里需要明确,AVPacket用于封装视频流buffer,AVFrame用于封装解码后的YUV数据,AVFrame中的数据可以通过 av_frame_move_ref 方法移动其内部存放的buffer,av_frame_unref给buffer减引用,引用为0就销毁buffer。后续在js层使用完毕后释放对象时,我们会使用这些方法,否则会造成浏览器内存泄露。

2.3 wasm数据传递给js层

解码完毕后,需要将YUV数据传递回js层,用于渲染。同样,这里也是通过注册C++类,映射一个对应的js类,在js层操作这个类,不同的是上一个我们创建的解码器会存在较长时间,而这里创建的视频帧类在使用完毕后需要立刻释放。

视频帧frame的C++类。其中wasm中的内存并不需要拷贝,可以直接通过emscripten::typed_memory_view 映射,在js层直接使用映射得到的内存句柄即可。这里把YUV的内存都进行了映射,同时还能返回视频帧的宽高和stride等信息。

#ifndef _VIDEO_FRAME_WRAPPER_H_
#define _VIDEO_FRAME_WRAPPER_H_#ifdef __cplusplus
extern "C" {
#endif#include <libavutil/frame.h>
#include <libavutil/imgutils.h>#ifdef __cplusplus
}
#endif#include <memory>
#include <iostream>
#include <emscripten/val.h>class VideoFrameWrapper : public std::enable_shared_from_this<VideoFrameWrapper>{public:VideoFrameWrapper(){}~VideoFrameWrapper(){Free();}int type() const { return type_; }uint8_t *data() const { return frame_->data[0]; }int linesizeY() const { return frame_->linesize[0]; }int linesizeU() const { return frame_->linesize[1]; }int linesizeV() const { return frame_->linesize[2]; }int width() const { return frame_->width; }int height() const { return frame_->height; }int format() const { return frame_->format; }double pts() const { return frame_->pts; }int data_ptr() const { return (int)(frame_->data[0]); }  // NOLINTint size() const {return av_image_get_buffer_size(AV_PIX_FMT_YUV420P, frame_->width, frame_->height, 1);}emscripten::val GetBytes() {return emscripten::val(emscripten::typed_memory_view(size(), frame_->data[0]));}emscripten::val GetBytesY() {return emscripten::val(emscripten::typed_memory_view(size(), frame_->data[0]));}emscripten::val GetBytesU() {return emscripten::val(emscripten::typed_memory_view(size(), frame_->data[1]));}emscripten::val GetBytesV() {return emscripten::val(emscripten::typed_memory_view(size(), frame_->data[2]));}std::shared_ptr<VideoFrameWrapper> Alloc(AVMediaType type, AVFrame *frame) {type_ = type;frame_ = frame;return shared_from_this();}void Free() {type_ = AVMEDIA_TYPE_UNKNOWN;if (frame_ != nullptr) {av_frame_unref(frame_);av_frame_free(&frame_);frame_ = nullptr;std::cout << "Frame::Free 1 this="<< (std::hex) <<this <<std::endl;}}private:int type_;AVFrame *frame_;
};#endif

VideoFrameWrapper 传递给js层。这里首先创建一个AVFrame,将解码后的内存转给这个AVFrame,之后创建VideoFrameWrapper,将其作为一个shared_ptr返回给js层。可见wasm可以将shared_ptr传递给js,那么js中也需要对shared_ptr进行管理。

void OpenAvcDecoder(emscripten::val lambda){std::cout<<"StreamDecoderWrapper::OpenAvcDecoder create"<<std::endl;decoder.OpenWithCodecID(AV_CODEC_ID_H264);decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{AVFrame *out_frame = av_frame_alloc();av_frame_move_ref(out_frame, frame);auto frame_wrapper = std::make_shared<VideoFrameWrapper>()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame);lambda(frame_wrapper);return 0;});}

VideoFrameWrapper注册js对象。注意,注册的时候要加一个smart_ptr,这个类在js层也会对对象进行引用操作。同时这里还注册了可以直接访问的属性。

#include <emscripten/bind.h>
#ifndef NDEBUG
#include <sanitizer/lsan_interface.h>
#endif#include "file_decoder_wrapper.h"
#include "stream_decoder_wrapper.h"
#include "video_frame_wrapper.h"using namespace emscripten;EMSCRIPTEN_BINDINGS(module){... ...    class_<VideoFrameWrapper>("VideoFrameWrapper").smart_ptr<std::shared_ptr<VideoFrameWrapper>>("shared_ptr<VideoFrameWrapper>").property("type", &VideoFrameWrapper::type).property("data", &VideoFrameWrapper::data_ptr).property("linesizeY", &VideoFrameWrapper::linesizeY).property("linesizeU", &VideoFrameWrapper::linesizeU).property("linesizeV", &VideoFrameWrapper::linesizeV).property("width", &VideoFrameWrapper::width).property("height", &VideoFrameWrapper::height).property("format", &VideoFrameWrapper::format).property("pts", &VideoFrameWrapper::pts).property("size", &VideoFrameWrapper::size).function("getBytes", &VideoFrameWrapper::GetBytes).function("getBytesY", &VideoFrameWrapper::GetBytesY).function("getBytesU", &VideoFrameWrapper::GetBytesU).function("getBytesV", &VideoFrameWrapper::GetBytesV);
}

js层调用。这里js层可以读取到回调对象的属性,还可以将其作为一个js对象传递,最终这个对象调用delete进行释放。

 openAvcDecoder(frame_callback){this.#stream_decoder_inner = new Module.StreamDecoderWrapper()this.#stream_decoder_inner.openAvcDecoder((videoFrameWrapperJS)=>{let w = videoFrameWrapperJS.width;let h = videoFrameWrapperJS.height;frame_callback(videoFrameWrapperJS)videoFrameWrapperJS.delete();})}

3.WebGL渲染

js层得到YUV的内存句柄就可以使用WebGL进行渲染。浏览器端WebGL可以直接将canvas作为画布,不需要EGL之类的复杂操作。外部获取canvas标签后,直接用其获取context,后续OpenGL操作在这个context上进行即可。

class WebGLPlayer {constructor(canvas) {this.canvas = canvas;this.gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");... ...}
}

shader编译,这里和一般OpenGL的shader操作一样,编译顶点和片元shader,获取顶点坐标和纹理坐标索引,获取YUV三个纹理的索引。

#init() {if (!this.gl) {console.log("[ERROR] WebGL not supported");return;}const gl = this.gl;gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);const program = gl.createProgram();const vertexShaderSource = ["attribute highp vec3 aPos;","attribute vec2 aTexCoord;","varying highp vec2 vTexCoord;","void main(void) {","  gl_Position = vec4(aPos, 1.0);","  vTexCoord = aTexCoord;","}",].join("\n");const vertexShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vertexShader, vertexShaderSource);gl.compileShader(vertexShader);{const msg = gl.getShaderInfoLog(vertexShader);if (msg) {console.log("[ERROR] Vertex shader compile failed");console.log(msg);}}const fragmentShaderSource = ["precision highp float;","varying lowp vec2 vTexCoord;","uniform sampler2D yTex;","uniform sampler2D uTex;","uniform sampler2D vTex;","const mat4 YUV2RGB = mat4(","  1.1643828125,             0, 1.59602734375, -.87078515625,","  1.1643828125, -.39176171875,    -.81296875,     .52959375,","  1.1643828125,   2.017234375,             0,  -1.081390625,","             0,             0,             0,             1",");","void main(void) {","  // gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 0., 1.0);","  gl_FragColor = vec4(","    texture2D(yTex, vTexCoord).x,","    texture2D(uTex, vTexCoord).x,","    texture2D(vTex, vTexCoord).x,","    1","  ) * YUV2RGB;","}",].join("\n");const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fragmentShader, fragmentShaderSource);gl.compileShader(fragmentShader);{const msg = gl.getShaderInfoLog(fragmentShader);if (msg) {console.log("[ERROR] Fragment shader compile failed");console.log(msg);}}gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);gl.linkProgram(program);gl.useProgram(program);if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {console.log("[ERROR] Shader link failed");}const vertices = new Float32Array([// positions      // texture coords-1.0, -1.0, 0.0,  0.0, 1.0,  // bottom left1.0, -1.0, 0.0,  1.0, 1.0,  // bottom right-1.0,  1.0, 0.0,  0.0, 0.0,  // top left1.0,  1.0, 0.0,  1.0, 0.0,  // top right])const verticesBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);const vertexPositionAttribute = gl.getAttribLocation(program, "aPos");gl.enableVertexAttribArray(vertexPositionAttribute);gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 20, 0);const textureCoordAttribute = gl.getAttribLocation(program, "aTexCoord");gl.enableVertexAttribArray(textureCoordAttribute);gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 20, 12);gl.y = new Texture(gl);gl.u = new Texture(gl);gl.v = new Texture(gl);gl.y.bind(0, program, "yTex");gl.u.bind(1, program, "uTex");gl.v.bind(2, program, "vTex");}

在得到我们上一部抛出的封装了解码数据的VideoFrameWrapper后就可以进行渲染了。这里注意ffmpeg解码后的YUV数据不一定是连续的,一定分别拿出AVFrame的每个分量,分别映射出来,否则可能会导致花屏。最终通过gl.y.fill  gl.u.fill  gl.v.fill 分别给yuv对应纹理上传buffer。这样就完成了渲染操作。

  render(frame) {if (!this.gl) {console.log("[ERROR] Render failed due to WebGL not supported");return;}const gl = this.gl;let port_width = gl.canvas.width;let port_height = gl.canvas.height;gl.viewport(0, 0, port_width, port_height);gl.clearColor(0.0, 0.0, 0.0, 0.0);gl.clear(gl.COLOR_BUFFER_BIT);const width = frame.width;const linesize = frame.linesize;const height = frame.height;const bytes = frame.bytes;const byteYLinesize = frame.linesizeY;const byteULinesize = frame.linesizeU;const byteVLinesize = frame.linesizeV;console.log('render width='+width+' linesizeY='+byteYLinesize)const len_y = byteYLinesize * height;const len_u = byteULinesize * height >> 1;const len_v = byteVLinesize * height >> 1;const byteY = frame.getBytesY()const byteU = frame.getBytesU()const byteV = frame.getBytesV()gl.y.fill(byteYLinesize, height, byteY.subarray(0, len_y));gl.u.fill(byteULinesize, height >> 1, byteU.subarray(0, len_u));gl.v.fill(byteVLinesize, height >> 1, byteV.subarray(0, len_v));gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);//gl.finish();//gl.commit();}

渲染播放丢帧问题

在实际操作中发现播放过程中丢帧严重,经过排查是在解码完成后直接抛出帧,由于解码时间不均匀导致有些帧播放后很快又被新帧覆盖,导致播放卡顿。目前在c层解码完毕后增加了一个delay操作,按照解码时间和帧率进行延迟等待,用于平滑渲染。此处考虑是否可以引入一个线程,或者是否有其比较好的解决方式。目前控制播放代码如下

decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{std::cout<<"OpenAvcDecoder debug2"<<std::endl;AVFrame *out_frame = av_frame_alloc();av_frame_move_ref(out_frame, frame);auto frame_wrapper = std::make_shared<VideoFrameWrapper>()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame);long delay_time = -1;long curr_ts = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();if(last_out_ts != 0){long curr_gap = curr_ts - last_out_ts;if(curr_gap > 0 && curr_gap < gap){delay_time = gap - curr_gap;} }last_out_ts = curr_ts;if(delay_time > 0){usleep(delay_time * 1000);std::cout<<"OpenAvcDecoder delay_time="<<delay_time<<std::endl;}lambda(frame_wrapper);return 0;});

4.Server端交互

server端与js端通过Websocket进行数据交互,目前提供了一个简单的协议头,用于请求视频和停止视频

//json data type == 1
//video data type == 2
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//|      'A'      |       'A'     |v=1|     type    |     rec       |
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//|                         payload length                          |
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

server端提供了一个H264FileVideoCapture类用于模拟视频采集和编码,如果需要使用自己的采集编码可以重新实现一个H264FileVideoCapture和MediaStreamer。

4.1 mongoose支持wasm多线程

wasm开启多线程,需要浏览器开启Cross-origin保护,否则直接报错。

这里需要mongoose在收到网页请求的时候,在响应头中增加设置,代码实现如下

      struct mg_http_serve_opts opts = {.root_dir = s_web_root};//wasm 多线程需要增加响应头opts.extra_headers = "Cross-Origin-Embedder-Policy:require-corp\r\nCross-Origin-Opener-Policy:same-origin\r\n";mg_http_serve_dir(c, (struct mg_http_message *)ev_data, &opts);

wasm 视频解码渲染实现相关推荐

  1. Android媒体解码MediaCodec,MediaExtractor

    Android提供了MediaPlayer播放器播放媒体文件,其实MediaPlyer只是对Android Media包下的MediaCodec和MediaExtractor进行了包装,方便使用.但是 ...

  2. Android视频解码及渲染

    今天给大家说说在android上如何做视频解码及渲染. 视频解码有多种方法,今天给大家介绍的是用android自带的MediaCodec进行硬解码,所谓硬解码就是利用硬件进行解码,速度快,与之相对就是 ...

  3. 美摄云非编系统——网页端实时编辑渲染方案

    美摄云非编是一款新型网页端非线性编辑工具,应用WebAssembly技术实现网页端直接渲染图像.本次LiveVideoStackCon 2020线上峰会我们邀请到了北京美摄网络科技有限公司的研发总监黄 ...

  4. 在线非线编系统——网页端实时编辑渲染方案

    本次我分享的主题是云非编系统,是一种web端视音频实时编辑渲染方案. 本次内容分为五个部分: 是美摄云非编方案的技术背景,也就是目前web端视音频编辑的现状以及我们采用新方案的原因: 是美摄云非编的技 ...

  5. 某网站视频加密的wasm略谈(二)

    某网站视频加密的wasm略谈(二) 网页反录制 第一种录制方式 第二种录制方式 网页端加密发展趋势 对于视频的加密: 对于代码的加密: 网页反录制 上一篇主要讲的是解密方向,那么这一篇主要讲的就是加密 ...

  6. python的openpyxl库如何读取特定列_通过渲染一百万个网页,来了解网络是如何崩溃的...

    最近在 medium 上看到这篇"比较新鲜的"文章 <We rendered a million web pages to learn how the web breaks& ...

  7. android 视频播放滤镜,用openGL ES+MediaPlayer 渲染播放视频+滤镜效果

    之前曾经写过用SurfaceView,TextureView+MediaPlayer 播放视频,和 ffmpeg avi解码后SurfaceView播放视频,今天再给大家来一篇openGL ES+Me ...

  8. ffmpeg 将拆分的数据合成一帧_FFmpeg + OpenGLES 实现视频解码播放和视频滤镜

    FFmpeg 开发系列连载: FFmpeg 开发(01):FFmpeg 编译和集成 FFmpeg 开发(02):FFmpeg + ANativeWindow 实现视频解码播放 FFmpeg 开发(03 ...

  9. 探寻浏览器渲染的秘密

    (给前端大全加星标,提升前端技能) 作者:前端桃园 公号 / 桃翁 前言 起因是这样,有运营小姐姐跟我反馈某个页面卡顿的厉害.心中突然一想,妈耶不会有bug吧,心慌慌的.然后自己打开页面,不卡呀,流畅 ...

最新文章

  1. linux系统用户属组,关于 Linux系统用户、组和权限管理
  2. java求一个数的阶乘_Java如何使用方法计算一个数字的阶乘值?
  3. bzoj2301: [HAOI2011]Problem b懵逼乌斯反演
  4. 北京化工大学计算机科专业,北京化工大学专业介绍及排名 哪些专业最好
  5. Mysql中的联合索引、前缀索引、覆盖索引
  6. CentOS7.0安装Nginx 1.7.4
  7. HMM_概率计算——forwar_algorithm实现
  8. 计算机和HMI设备通信之程序上下载
  9. linux容器返回宿主机,Linux下Docker容器访问宿主机网络
  10. python爬虫天气预报难不难_Python爬虫天气预报实例详解(小白入门)
  11. 京东价格监控软件开发技术探讨八:如何获取京东商品分类数据
  12. PuTTY怎么读,PuTTY怎么发音,PuTTY的发音
  13. 华为P50/P50Pro怎么解锁huawei P50pro屏幕锁开机锁激活设备锁了应该如何强制解除鸿蒙系统刷机解锁方法流程步骤不开机跳过锁屏移除锁定进系统方法经验
  14. llama是什么动物_羊驼(Alpaca)与骆马(Llama)
  15. 鸿蒙系统1004无标题,win10 10041更新提示错误怎么处理
  16. picgo+sharex写markdown笔记
  17. SGU 111 Very simple problem 翻译 题解
  18. mysql统计近n天每天的数据量
  19. Linux之alias取别名
  20. VBS识别网页验证码

热门文章

  1. 《结合DDD讲清楚编写技术方案的七大维度》再讨论
  2. 远望资本程浩:做 To B,一定要避免 9 类错误!
  3. 手机端PC端判断 微信浏览器支付宝浏览器判断
  4. 如何在Windows中调整鼠标设置
  5. excel 2007 直方图 条图 黑白 条纹填充
  6. sftp之linux下修改端口
  7. 轻量化网络(五)ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices
  8. 找回不见的本地连接【摘】
  9. STM32F101xxT6中VBAT 管脚上的怪现象
  10. 强化学习分类与汇总介绍