Android ffmpeg解码

最近弄了下android ffmpeg解码,下面把流程总结一下方便大家参考

1.ffmpeg移植

网上有一些关于ffmpeg移植的文章,试了很多次但是最后生成的libffmpeg.so就只有几KB所以这里换一种方式,网上也有文章说到了,其实我觉得这种方式反而要好一点,只需要生成一个库就行了。

我修改好的ffmpeg下载地址:http://download.csdn.net/detail/hclydao/6865961

下载后,解压进入ffmpeg-1.2.4-android目录,里面有一个mkconfig.sh文件,打开这个文件,你需要修改几个地方.

mkconfig.sh内容如下:

[python]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #!/bin/sh  
  2. export PREBUILT=/dao/work/tools/android-ndk-r5b/toolchains/arm-linux-androideabi-4.4.3  
  3. export PLATFORM=/dao/work/tools/android-ndk-r5b/platforms/android-9/arch-arm  
  4. export TMPDIR=/dao/tmp  
  5.   
  6. ./configure \  
  7. --target-os=linux \  
  8. --arch=arm \  
  9. --disable-ffmpeg \  
  10. --disable-ffplay \  
  11. --disable-ffprobe \  
  12. --disable-ffserver \  
  13. --disable-avdevice \  
  14. --disable-avfilter \  
  15. --disable-postproc \  
  16. --disable-swresample \  
  17. --disable-avresample \  
  18. --disable-symver \  
  19. --disable-debug \  
  20. --disable-stripping \  
  21. --disable-yasm \  
  22. --disable-asm \  
  23. --enable-gpl \  
  24. --enable-version3 \  
  25. --enable-nonfree \  
  26. --disable-doc \  
  27. --enable-static \  
  28. --disable-shared \  
  29. --enable-cross-compile \  
  30. --prefix=/dao/_install \  
  31. --cc=$PREBUILT/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc \  
  32. --cross-prefix=$PREBUILT/prebuilt/linux-x86/bin/arm-linux-androideabi- \  
  33. --nm=$PREBUILT/prebuilt/linux-x86/bin/arm-linux-androideabi-nm \  
  34. --extra-cflags="-fPIC -DANDROID -I$PLATFORM/usr/include" \  
  35. --extra-ldflags="-L$PLATFORM/usr/lib -nostdlib"  
  36.   
  37. sed -i 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h    
  38. sed -i 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h    
  39. sed -i 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h    
  40. sed -i 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h    
  41. sed -i 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h    
  42. sed -i 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h    
  43. sed -i 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h    
  44. sed -i 's/HAVE_CBRTF 0/HAVE_CBRTF 1/g' config.h    
  45. sed -i 's/HAVE_ISINF 0/HAVE_ISINF 1/g' config.h    
  46. sed -i 's/HAVE_ISNAN 0/HAVE_ISNAN 1/g' config.h    
  47. sed -i 's/HAVE_SINF 0/HAVE_SINF 1/g' config.h    
  48. sed -i 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h  
  49. sed -i 's/#define av_restrict restrict/#define av_restrict/g' config.h  
最开始的环境变量要设置一下:

[python]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. export PREBUILT=/dao/work/tools/android-ndk-r5b/toolchains/arm-linux-androideabi-4.4.3  
  2. export PLATFORM=/dao/work/tools/android-ndk-r5b/platforms/android-9/arch-arm  
  3. export TMPDIR=/dao/tmp  
这个为你的NDK的路径,以及临时目录

还有一个地方需要修改:

[python]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. --prefix=/dao/_install \  
这个为安装目录,请改成你自己的。

然后执行./mkconfig.sh

应该会有一个警告,不用管。直接执行make几分钟左右应该就会编译完成了,然后执行make install

在你的安装目录下就会生成两个目录include和llib到这里 我们移植就完成了,先把这些文件放在这,后面我们需要用到。

2.jni的编写

在你的android工程目录下新建一个jni的目录(其实我是在另一个工程里新建的,前面我试了一次执行ndk-build的时候把工程里的东西给删除了),把前面我们安装的include整个目录拷贝到jni目录下,把lib目录里的所有.a文件拷贝到jni目录下,新建Android.mk文件,内容如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. LOCAL_PATH := $(call my-dir)  
  2.   
  3. include $(CLEAR_VARS)  
  4. LOCAL_MODULE := avformat  
  5. LOCAL_SRC_FILES := libavformat.a  
  6. LOCAL_CFLAGS :=-Ilibavformat  
  7. LOCAL_EXPORT_C_INCLUDES := libavformat  
  8. LOCAL_EXPORT_CFLAGS := -Ilibavformat  
  9. LOCAL_EXPORT_LDLIBS := -llog  
  10. include $(PREBUILT_STATIC_LIBRARY)  
  11.   
  12. include $(CLEAR_VARS)  
  13. LOCAL_MODULE := avcodec  
  14. LOCAL_SRC_FILES := libavcodec.a  
  15. LOCAL_CFLAGS :=-Ilibavcodec  
  16. LOCAL_EXPORT_C_INCLUDES := libavcodec  
  17. LOCAL_EXPORT_CFLAGS := -Ilibavcodec  
  18. LOCAL_EXPORT_LDLIBS := -llog  
  19. include $(PREBUILT_STATIC_LIBRARY)  
  20.   
  21. include $(CLEAR_VARS)  
  22. LOCAL_MODULE := avutil  
  23. LOCAL_SRC_FILES := libavutil.a  
  24. LOCAL_CFLAGS :=-Ilibavutil  
  25. LOCAL_EXPORT_C_INCLUDES := libavutil  
  26. LOCAL_EXPORT_CFLAGS := -Ilibavutil  
  27. LOCAL_EXPORT_LDLIBS := -llog  
  28. include $(PREBUILT_STATIC_LIBRARY)  
  29.   
  30. include $(CLEAR_VARS)  
  31. LOCAL_MODULE := swscale  
  32. LOCAL_SRC_FILES :=libswscale.a  
  33. LOCAL_CFLAGS :=-Ilibavutil -Ilibswscale  
  34. LOCAL_EXPORT_C_INCLUDES := libswscale  
  35. LOCAL_EXPORT_CFLAGS := -Ilibswscale  
  36. LOCAL_EXPORT_LDLIBS := -llog -lavutil  
  37. include $(PREBUILT_STATIC_LIBRARY)  
  38.   
  39. include $(CLEAR_VARS)  
  40. LOCAL_MODULE    := ffmpegutils  
  41. LOCAL_SRC_FILES := native.c  
  42. LOCAL_C_INCLUDES := $(LOCAL_PATH)/include  
  43. LOCAL_LDLIBS    := -L$(LOCAL_PATH) -lm -lz  
  44. LOCAL_STATIC_LIBRARIES := avformat avcodec avutil swscale  
  45. include $(BUILD_SHARED_LIBRARY)  
新建native.c文件,这个是我们最终要调用到的文件,内容如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /* 
  2.  * Copyright 2011 - Churn Labs, LLC 
  3.  * 
  4.  * Licensed under the Apache License, Version 2.0 (the "License"); 
  5.  * you may not use this file except in compliance with the License. 
  6.  * You may obtain a copy of the License at 
  7.  * 
  8.  *     http://www.apache.org/licenses/LICENSE-2.0 
  9.  * 
  10.  * Unless required by applicable law or agreed to in writing, software 
  11.  * distributed under the License is distributed on an "AS IS" BASIS, 
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  13.  * See the License for the specific language governing permissions and 
  14.  * limitations under the License. 
  15.  */  
  16.   
  17. /* 
  18.  * This is mostly based off of the FFMPEG tutorial: 
  19.  * http://dranger.com/ffmpeg/ 
  20.  * With a few updates to support Android output mechanisms and to update 
  21.  * places where the APIs have shifted. 
  22.  */  
  23.   
  24. #include <jni.h>  
  25. #include <string.h>  
  26. #include <stdio.h>  
  27. #include <android/log.h>  
  28.   
  29. #include <libavcodec/avcodec.h>  
  30. #include <libavformat/avformat.h>  
  31. #include <libswscale/swscale.h>  
  32.   
  33. #define  LOG_TAG    "FFMPEGSample"  
  34. #define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)  
  35. #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)  
  36.   
  37. AVCodecContext * pCodecCtx = NULL;  
  38. AVFrame * pFrame=NULL;  
  39. AVPacket avpkt;  
  40. struct SwsContext *swsctx = NULL;  
  41. AVFrame * picture=NULL;  
  42.   
  43. JNIEXPORT jint JNICALL Java_com_dao_iclient_FfmpegIF_getffmpegv(JNIEnv * env, jclass obj)  
  44. {  
  45.     LOGI("getffmpegv");  
  46.     return avformat_version();  
  47. }  
  48.   
  49. JNIEXPORT jint JNICALL Java_com_dao_iclient_FfmpegIF_DecodeInit(JNIEnv * env, jclass obj,jint width,jint height)  
  50. {  
  51.     LOGI("Decode_init");  
  52.   
  53.     AVCodec * pCodec=NULL;  
  54.     avcodec_register_all();  
  55.     //av_register_all();  
  56.     //avcodec_init();  
  57.     av_init_packet(&avpkt);  
  58.     pCodec=avcodec_find_decoder(CODEC_ID_H264);  
  59.     if(NULL!=pCodec)  
  60.     {  
  61.         pCodecCtx=avcodec_alloc_context3(pCodec);  
  62.         if(avcodec_open2(pCodecCtx,pCodec,NULL)>=0)  
  63.         {  
  64.             pCodecCtx->height = height;  
  65.             pCodecCtx->width = width;  
  66.   
  67.             pFrame=avcodec_alloc_frame();  
  68.         }  
  69.         return 1;  
  70.     }   
  71.     else  
  72.         return 0;  
  73. }  
  74.   
  75. JNIEXPORT jint JNICALL Java_com_dao_iclient_FfmpegIF_Decoding(JNIEnv * env, jclass obj,const jbyteArray pSrcData,const jint DataLen,const jbyteArray pDeData)  
  76. {  
  77.     //LOGI("Decoding");  
  78.   
  79.     int frameFinished;  
  80.     int i,j;  
  81.     int consumed_bytes;  
  82.     jbyte * Buf = (jbyte*)(*env)->GetByteArrayElements(env, pSrcData, 0);  
  83.     jbyte * Pixel= (jbyte*)(*env)->GetByteArrayElements(env, pDeData, 0);  
  84.      
  85.     avpkt.data = Buf;  
  86.     avpkt.size = DataLen;  
  87.     consumed_bytes=avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,&avpkt);  
  88.     //av_free_packet(&avpkt);  
  89.     if(frameFinished) {  
  90.         picture=avcodec_alloc_frame();  
  91.         avpicture_fill((AVPicture *) picture, (uint8_t *)Pixel, PIX_FMT_RGB565,pCodecCtx->width,pCodecCtx->height);  
  92.             swsctx = sws_getContext(pCodecCtx->width,pCodecCtx->height, pCodecCtx->pix_fmt,    pCodecCtx->width, pCodecCtx->height,PIX_FMT_RGB565, SWS_BICUBIC, NULL, NULL, NULL);  
  93.         sws_scale(swsctx,(const uint8_t* const*)pFrame->data,pFrame->linesize,0,pCodecCtx->height,picture->data,picture->linesize);  
  94.     }  
  95.       
  96.     (*env)->ReleaseByteArrayElements(env, pSrcData, Buf, 0);  
  97.     (*env)->ReleaseByteArrayElements(env, pDeData, Pixel, 0);  
  98.     return  consumed_bytes;  
  99. }  
  100.   
  101. JNIEXPORT jint JNICALL Java_com_dao_iclient_FfmpegIF_DecodeRelease(JNIEnv * env, jclass obj)  
  102. {  
  103.     //LOGI("Decode_release");  
  104.     sws_freeContext(swsctx);  
  105.     av_free_packet(&avpkt);  
  106.     av_free(pFrame);  
  107.     av_free(picture);  
  108.     avcodec_close(pCodecCtx);  
  109.     av_free(pCodecCtx);  
  110.    
  111.     return 1;  
  112. }  

最后我们只用到了两个函数一个是init一个是decoding,这里说明一下函数的命名方式为Java_包名_类名_函数名,com.dao.iclient是我的包名,FfmpegIF是我的类名,DecodeInit是我的函数名.如果你之前已经在系统设置了ndk的环境变量,你就可以直接在工程目录下执行ndk-build(这里目录别弄错了).最后会在工程目录下的libs/armeabi/生成这个库文件.

3.应用ffmpeg库

增加FfmpegIF类,内容如下

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package com.dao.iclient;  
  2.   
  3. public class FfmpegIF {  
  4.     public static short TYPE_MODE_DATA = 0;  
  5.     public static short TYPE_MODE_COM = 1;  
  6.     public static int VIDEO_COM_START = 0x00;  
  7.     public static int VIDEO_COM_POSE = 0x01;  
  8.     public static int VIDEO_COM_RUN = 0x02;  
  9.     public static int VIDEO_COM_ACK = 0x03;  
  10.     public static int VIDEO_COM_STOP = 0x04;  
  11.       
  12.     static public native int getffmpegv();  
  13.     static public native int DecodeInit(int width,int height);  
  14.     static public native int Decoding(byte[] in,int datalen,byte[] out);  
  15.     static public native int DecodeRelease();  
  16.     static {  
  17.         System.loadLibrary("ffmpegutils");  
  18.     }  
  19.   
  20. }  
在布局文件里增加一个ImageView对象,我这里是接收到网络传输过来的数据后进行的解码,我把代码都帖上来吧,以下是我的主文件的内容:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package com.dao.iclient;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.io.OutputStream;  
  6. import java.net.Socket;  
  7. import java.nio.ByteBuffer;  
  8. import java.util.Timer;  
  9. import java.util.TimerTask;  
  10.   
  11. import android.app.Activity;  
  12. import android.graphics.Bitmap;  
  13. import android.graphics.Bitmap.Config;  
  14. import android.os.Bundle;  
  15. import android.os.Handler;  
  16. import android.os.Message;  
  17. import android.os.SystemClock;  
  18. import android.view.Menu;  
  19. import android.widget.ImageView;  
  20.   
  21. public class IcoolClient extends Activity {  
  22.     public Socket socket;  
  23.     public ByteBuffer buffer;  
  24.     public ByteBuffer Imagbuf;  
  25.     //net package  
  26.     public static short type = 0;  
  27.     public static int packageLen = 0;  
  28.     public static int sendDeviceID = 0;  
  29.     public static int revceiveDeviceID = 0;  
  30.     public static short sendDeviceType = 0;  
  31.     public static int dataIndex = 0;  
  32.     public static int dataLen = 0;  
  33.     public static int frameNum = 0;  
  34.     public static int commType = 0;  
  35.     //size  
  36.     public static int packagesize;  
  37.       
  38.     public OutputStream outputStream=null;  
  39.     public InputStream inputStream=null;  
  40.       
  41.     public int width = 0;  
  42.     public int height = 0;  
  43.     public Bitmap VideoBit;  
  44.     public ImageView mImag;  
  45.       
  46.     public byte[] mout;  
  47.     protected static final int REFRESH = 0;  
  48.     private Handler mHandler;  
  49.     @Override  
  50.     protected void onCreate(Bundle savedInstanceState) {  
  51.         super.onCreate(savedInstanceState);  
  52.         setContentView(R.layout.activity_icool_client);  
  53.         mImag = (ImageView)findViewById(R.id.mimg);  
  54.         packagesize = 7 * 4 + 2 * 2;  
  55.         buffer = ByteBuffer.allocate(packagesize);  
  56.        // int ffpmegv = FfmpegIF.getffmpegv();  
  57.         //System.out.println("ffmpeg version is " + ffpmegv);  
  58.         width = 640;  
  59.         height = 480;  
  60.         mout = new byte[width * height * 2];  
  61.         Imagbuf = ByteBuffer.wrap(mout);  
  62.         VideoBit = Bitmap.createBitmap(width ,height, Config.RGB_565);  
  63.         //mImag.postInvalidate();  
  64.         int ret = FfmpegIF.DecodeInit(width, height);  
  65.         //System.out.println(" ret is " + ret);  
  66.         mHandler = new Handler();  
  67.         new StartThread().start();  
  68.     }  
  69.   
  70.     final Runnable mUpdateUI = new Runnable() {  
  71.   
  72.         @Override  
  73.         public void run() {  
  74.             // TODO Auto-generated method stub  
  75.             VideoBit.copyPixelsFromBuffer(Imagbuf);  
  76.             mImag.setImageBitmap(VideoBit);  
  77.         }  
  78.     };  
  79.       
  80.     class StartThread extends Thread {  
  81.   
  82.         @Override  
  83.         public void run() {  
  84.             // TODO Auto-generated method stub  
  85.             //super.run();            
  86.             int datasize;  
  87.             try {  
  88.                 socket = new Socket("192.168.1.15"9876);  
  89.                 //System.out.println("socket");  
  90.                 SendCom(FfmpegIF.VIDEO_COM_STOP);  
  91.                 SendCom(FfmpegIF.VIDEO_COM_START);  
  92.                 //new ShowBuffer().start();  
  93.                 inputStream = socket.getInputStream();  
  94.                 byte[] Rbuffer = new byte[packagesize];  
  95.                 while(true) {  
  96.                     inputStream.read(Rbuffer);  
  97.                     //byte2hex(Rbuffer);  
  98.                     SystemClock.sleep(3);  
  99.                     datasize = getDataL(Rbuffer);  
  100.                     if(datasize > 0) {  
  101.                         byte[] Data = new byte[datasize];  
  102.                         int size;  
  103.                         size = inputStream.read(Data);  
  104.                         FfmpegIF.Decoding(Data, size, mout);  
  105.                         //VideoBit.copyPixelsFromBuffer(Imagbuf);  
  106.                         //mImag.setImageBitmap(VideoBit);  
  107.                         mHandler.post(mUpdateUI);  
  108.                         //System.out.println("read datalen is " + size);  
  109.                         //SystemClock.sleep(10);  
  110.                         SendCom(FfmpegIF.VIDEO_COM_ACK);  
  111.                     }  
  112.                 }  
  113.             }catch (IOException e) {  
  114.                 e.printStackTrace();  
  115.             }  
  116.         }  
  117.     }  
  118.   
  119.     public void SendCom(int comtype) {  
  120.         byte[] Bbuffer = new byte[packagesize];  
  121.         try {  
  122.             outputStream = socket.getOutputStream();  
  123.             type = FfmpegIF.TYPE_MODE_COM;  
  124.             packageLen = packagesize;  
  125.             commType = comtype;  
  126.             putbuffer();  
  127.             Bbuffer = buffer.array();  
  128.             outputStream.write(Bbuffer);  
  129.             //System.out.println("send done");  
  130.         } catch (IOException e) {  
  131.             e.printStackTrace();  
  132.         }   
  133.     }  
  134.       
  135.     public void putbuffer(){  
  136.         buffer.clear();  
  137.         buffer.put(ShorttoByteArray(type));  
  138.         buffer.put(InttoByteArray(packageLen));  
  139.         buffer.put(InttoByteArray(sendDeviceID));  
  140.         buffer.put(InttoByteArray(revceiveDeviceID));  
  141.         buffer.put(ShorttoByteArray(sendDeviceType));  
  142.         buffer.put(InttoByteArray(dataIndex));  
  143.         buffer.put(InttoByteArray(dataLen));  
  144.         buffer.put(InttoByteArray(frameNum));  
  145.         buffer.put(InttoByteArray(commType));  
  146.         //System.out.println("putbuffer done");  
  147.     }  
  148.       
  149.     private static byte[] ShorttoByteArray(short n) {  
  150.         byte[] b = new byte[2];  
  151.         b[0] = (byte) (n & 0xff);  
  152.         b[1] = (byte) (n >> 8 & 0xff);  
  153.         return b;  
  154.     }  
  155.     
  156.     private static byte[] InttoByteArray(int n) {  
  157.         byte[] b = new byte[4];  
  158.         b[0] = (byte) (n & 0xff);  
  159.         b[1] = (byte) (n >> 8 & 0xff);  
  160.         b[2] = (byte) (n >> 16 & 0xff);  
  161.         b[3] = (byte) (n >> 24 & 0xff);  
  162.         return b;  
  163.     }  
  164.      
  165.     public short getType(byte[] tpbuffer){  
  166.         short gtype = (short) ((short)tpbuffer[0] + (short)(tpbuffer[1] << 8));  
  167.         //System.out.println("gtype is " + gtype);  
  168.         return gtype;  
  169.     }  
  170.       
  171.     public int getPakL(byte[] pkbuffer){  
  172.         int gPackageLen = ((int)(pkbuffer[2]) & 0xff) | ((int)(pkbuffer[3] & 0xff) << 8) | ((int)(pkbuffer[4] & 0xff) << 16) | ((int)(pkbuffer[5] & 0xff) << 24);  
  173.         //System.out.println("gPackageLen is " + gPackageLen);  
  174.         return gPackageLen;  
  175.     }  
  176.       
  177.     public int getDataL(byte[] getbuffer){  
  178.         int gDataLen = (((int)(getbuffer[20] & 0xff)) | ((int)(getbuffer[21] & 0xff) << 8) | ((int)(getbuffer[22] & 0xff) << 16) | ((int)(getbuffer[23] & 0xff) << 24));  
  179.         //System.out.println("gDataLen is " + gDataLen);  
  180.         return gDataLen;  
  181.     }  
  182.       
  183.     public int getFrameN(byte[] getbuffer){  
  184.         int getFrameN = (int)(((int)(getbuffer[24])) + ((int)(getbuffer[25]) << 8) + ((int)(getbuffer[26]) << 16) + ((int)(getbuffer[27]) << 24));  
  185.         //System.out.println("getFrameN is " + getFrameN);  
  186.         return getFrameN;  
  187.     }  
  188.       
  189.     private void byte2hex(byte [] buffer) {    
  190.         String h = "";    
  191.         for(int i = 0; i < buffer.length; i++){    
  192.             String temp = Integer.toHexString(buffer[i] & 0xFF);  
  193.             if(temp.length() == 1){  
  194.                 temp = "0" + temp;    
  195.             }    
  196.             h = h + " "+ temp;    
  197.         }    
  198.        // System.out.println(h);  
  199.     }  
  200.   
  201.     @Override  
  202.     public boolean onCreateOptionsMenu(Menu menu) {  
  203.         // Inflate the menu; this adds items to the action bar if it is present.  
  204.         getMenuInflater().inflate(R.menu.icool_client, menu);  
  205.         return true;  
  206.     }  
  207.   
  208. }  

函数说明:

mUpdateUI用于解决主进程无法刷新UI的问题

StartThread网络通信线程主要工作是在这里,先接收指令包,然后接收数据包,然后解码显示

SendCom指令包发送

putbuffer指令包生成,因为服务器是用C写的,指令包是一个结构体所以这里进行了这样的处理,同时需要注意字节对齐,C语言的long类型是4个字节,Java的long类型为8个字节,这里需要注意,我在这纠结了几个小时。

ShorttoByteArray,InttoByteArray网络通信格式转换,低字节在前高字节在后

getType,getPakL,getDataL,getFrameN,指令包相关数据获取

byte2hex将byte数组转换成十六进制输出,这里是为了调试用.

============================================
作者:hclydao
http://blog.csdn.net/hclydao
版权没有,但是转载请保留此段声明

============================================

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值