偶遇FFMpeg(四)-FFmpeg 推流(PC端&Android端)

开编

之前在Android集成FFmpeg。主要还是基于命令行的方式进行操作。刚刚好最近又在研究推流相关的东西。看了一些博文。和做了一些实践。
就希望通过本文记录袭来。
本文的大体结构如下


目录.png

PC端

FFMPEG 开发环境搭建

笔者是在 Windows10 64+Visual Studio2017的环境下开发的

下载和安装VisualStudio2017

去官网下载和安装就可以

在项目中配置FFMPEG

  1. 下载FFMPEG相关的文件和解压
    FFMPEG WINDOW BUILD中下载 devshared两个部分的内容
    下载示例图.png
  • dev压缩包内

    dev_package.png

  • shared压缩包内

    shared_package.png

  1. 创建VisualStudio项目和配置FFMPEG
  • 创建控制台项目


    创建VisualStudio项目.png
  • 在项目中配置依赖项(重点)

  • 在左上角,点击项目。最后一下的弹出框中进行配置。


    项目相关配置.png
  • 然后将dll的文件复制到当前的目录下。


    文件复制到当前.png
  • 将Window编译调试,选择到正确的x64


    正确的x64.png
  • 处理一些错误。让程序跑起来

  • 错误1: av_register_all过时。
    解决方法: 暂时没有什么更好的办法,只能去头文件里面。把attribute_deprecated注释掉了

推流代码

大致先了解一下结构体和结构体之间的关系

结构体关系

结构体关系.png

结构体

  • AVFormatContext
    AVFormatContext是格式封装的上下文对象。
    在这里,会比较熟悉的常用的成员变量有:

    • AVIOContext *pb:用来合成音频和视频,或者分解的AVIOContext
    • unsigned int nb_streams:视音频流的个数
    • AVStream **streams:视音频流
    • char filename[1024]:文件名
    • AVDictionary *metadata:存储视频元信息的metadata对象。
  • AVDictionaryEntry
    每一条元数据分为keyvalue两个属性。

typedef struct AVDictionaryEntry {
    char *key;
    char *value;
} AVDictionaryEntry;

可以根据下面代码。取出这些数据

    AVFormatContext *fmt_ctx = NULL;
    AVDictionaryEntry *tag = NULL;
    int ret;

    if ((ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL)))
        return ret;

    while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
        printf("%s=%s\n", tag->key, tag->value);

    avformat_close_input(&fmt_ctx);
  • AVRational
    表示媒体信息的一些分数,是分母和分子的结构。计算过程中,会多次使用这样的数据结构
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;
  • AVPacket
    AVPacket是存储压缩编码数据相关信息的结构体。
    • uint8_t *data:压缩编码的数据。
      例如对于H.264来说。1个AVPacket的data通常对应一个NAL!
      注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
      因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。

      -int size
      data的大小
    • int64_t pts
      显示时间戳
      -int64_t dts
      解码时间戳
      -int stream_index
      标识该AVPacket所属的视频/音频流。

FFMPEG推流的套路

套路图如下:


FFMPEG推流的套路.png

整个方法的流向:

copy from leixiaohua.png

首先,我们先来熟悉一下这个整体的套路。其实推流的过程。我的理解是,经过解封装,按照原来的数据结构,提取和转成目标数据结构进行发送。
因为FFmpeg做好了封装,我们只要对其调用方法就可以了。

按照套路图,我们知道,使用FFmpeg的话

  1. 第一步是得到整体封装的输入和输出的上下文对象AVFormatContext
    //注册所有的
    av_register_all();
    //初始化网络
    avformat_network_init();
    //配置输入和输出
    const char *inUrl = "dongfengpo.flv";
    const char *outUrl = "rtmp://localhost/live/test";

    AVFormatContext *ictx = NULL;
        //得到输入的上下文
    int ret = avformat_open_input(&ictx, inUrl, NULL, NULL);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << " avformat_open_input success! " << endl;

    //去打印结果
    ret = avformat_find_stream_info(ictx, NULL);
    if (ret < 0)
    {
        return avError2(ret);
    }

    //将AVFormat打印出来
    av_dump_format(ictx, 0, inUrl, 0);

    //开始处理输出流
    int videoIndex = 0;
    //0.先得到AVFormat
    AVFormatContext *octx;
    AVOutputFormat *ofmt = NULL;

    ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << "avformat_alloc_output_context2 success!" << endl;

    ofmt = octx->oformat;

  1. 再创建输出的AVStream,并从输入AVFormatContext的其中取得AVStream,将对应的参数(主要是编码器信息)copy到其中。
        //开始遍历流,进行对应stream的创建
    for (int i = 0; i < ictx->nb_streams; i++)
    {
        //这里开始要创建一个新的AVStream
        AVStream *stream = ictx->streams[i];

        //判断是否是videoIndex。这里先记录下视频流。后面会对这个流进行操作
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoIndex = i;
        }

               //创建输出流
        AVCodec *c = avcodec_find_decoder(stream->codecpar->codec_id);
        AVStream *os = avformat_new_stream(octx, c);

        //应该将编解码器的参数从input中复制过来
                // 这里要注意的是,因为 os->codec这样的取法,已经过时了。所以使用codecpar
        ret = avcodec_parameters_copy(os->codecpar, stream->codecpar);
        if (ret < 0)
        {
            return avError2(ret);
        }
        cout << "avcodec_parameters_copy success!" << endl;
        cout << "avcodec_parameters_copy success! in stream codec tag" << stream->codecpar->codec_tag << endl;
        cout << "avcodec_parameters_copy success! out stream  codec tag" << os->codecpar->codec_tag << endl;

        //复制成功之后。还需要设置 codec_tag(编码器的信息?)
        os->codecpar->codec_tag = 0;
    }

    //检查一遍我们的输出
    av_dump_format(octx, 0, outUrl, 1);
  1. 因为是推流,所以第三部,就是通过avio_open链接网址,做好推流的准备
        //开始使用io进行推流
        //通过AVIO_FLAG_WRITE这个标记位,打开输出的AVFormatContext->AVIOContext
    ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << "avio_open success!" << endl;
  1. 推流的过程。首先通过 avformat_write_header写入头部信息。接着是通过av_read_frame函数读取输入的frame的数据,写入到AVPakcet 当中。处理每一帧的ptsdts。再通过av_interleaved_write_frame将这一个帧发送出去。最后,通过av_packet_unref释放AVPacket
    //先写头
    ret = avformat_write_header(octx, 0);
    if (ret < 0)
    {
        return avError2(ret);
    }
    //取得到每一帧的数据,写入
    AVPacket pkt;

    //为了让我们的代码发送流的速度,相当于整个视频播放的数据。需要记录程序开始的时间
    //后面再根据,每一帧的时间。做适当的延迟,防止我们的代码发送的太快了
    long long start_time = av_gettime();
    //记录视频帧的index,用来计算pts
    long long frame_index = 0;

    while (true)
    {
        //输入输出视频流
        AVStream *in_stream, *out_stream;

        //从输入流中读取数据 frame到AVPacket当中
        ret = av_read_frame(ictx, &pkt);
        if (ret < 0)
        {
            break;
        }

        //没有显示时间的时候,才会进入计算和校验
        //没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数。如果没有pts,则进行计算
        if (pkt.pts == AV_NOPTS_VALUE)
        {
            //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
             //先得到流中的time_base
            AVRational time_base = ictx->streams[videoIndex]->time_base;
            //开始校对pts和 dts.通过time_base和dts转成真正的时间
            //得到的是每一帧的时间
            /*
            r_frame_rate 基流帧速率 。取得是时间戳内最小的帧的速率 。每一帧的时间就是等于 time_base/r_frame_rate
            av_q2d 转化为double类型
            */
            int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ictx->streams[videoIndex]->r_frame_rate);
            //配置参数  这些时间,都是通过 av_q2d(time_base) * AV_TIME_BASE 来转成实际的参数
            pkt.pts = (double)(frame_index * calc_duration) / (double)av_q2d(time_base) * AV_TIME_BASE;
            //一个GOP中,如果存在B帧的话,只有I帧的dts就不等于pts
            pkt.dts = pkt.pts;
            pkt.duration = (double)calc_duration / (double)av_q2d(time_base) * AV_TIME_BASE;
        }

        //开始处理延迟.只有等于视频的帧,才会处理
        if (pkt.stream_index == videoIndex)
        {
            //需要计算当前处理的时间和开始处理时间之间的间隔??

            //0.先取时间基数
            AVRational time_base = ictx->streams[videoIndex]->time_base;

            //AV_TIME_BASE_Q 用小数表示的时间基数。等于时间基数的倒数
            AVRational time_base_r = { 1, AV_TIME_BASE };

            //计算视频播放的时间. 公式等于 pkt.dts * time_base / time_base_r`
            //.其实就是 stream中的time_base和定义的time_base直接的比例
            int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_r);
            //计算实际视频的播放时间。 视频实际播放的时间=代码处理的时间??
            int64_t now_time = av_gettime() - start_time;

            cout << time_base.num << " " << time_base.den << "  " << pkt.dts << "  " << pkt.pts << "   " << pts_time << endl;
            //如果显示的pts time 比当前的时间迟,就需要手动让程序睡一会,再发送出去,保持当前的发送时间和pts相同
            if (pts_time > now_time)
            {
                //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                av_usleep((unsigned int)(pts_time - now_time));
            }
        }

                //重新计算一次pts和dts.主要是通过 in_s的time_base 和 out_s的time_base进行计算和校对
        //先取得stream
        in_stream = ictx->streams[pkt.stream_index];
        out_stream = octx->streams[pkt.stream_index];

        //重新开始指定时间戳
        //计算延时后,重新指定时间戳。 这次是根据 in_stream 和 output_stream之间的比例
        //计算dts时,不再直接用pts,因为如有有B帧,就会不同
        //pts,dts,duration都也相同
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt.duration = (int)av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        //再次标记字节流的位置,-1表示不知道字节流的位置
        pkt.pos = -1;

        //如果当前的帧是视频帧,则将我们定义的frame_index往后推
        if (pkt.stream_index == videoIndex)
        {
            printf("Send %8d video frames to output URL\n", frame_index);
            frame_index++;
        }

        //发送!!!
        ret = av_interleaved_write_frame(octx, &pkt);
        if (ret < 0)
        {
            printf("发送数据包出错\n");
            break;
        }

        //使用完了,记得释放
        av_packet_unref(&pkt);
    }
    //写文件尾(Write file trailer)  
    av_write_trailer(octx);
    avformat_close_input(&ictx);
    /* close output */
    if (ictx && !(octx->flags & AVFMT_NOFILE))
        avio_close(octx->pb);
    avformat_free_context(octx);
    if (ret < 0 && ret != AVERROR_EOF) {
        printf("Error occurred.\n");
        return -1;
    }

Android端

基本的代码逻辑和上面是一致的。只是需要集成到Android当中。

集成

  • 将编译好的ffmpeg复制到libs下


    image.png
  • 编写Cmake文件
    将ffmpeg加入链接库。同时将include的文件路径设置正确
cmake_minimum_required(VERSION 3.4.1)

set(INC_DIR ${CMAKE_SOURCE_DIR}/libs/include)
set(LINK_DIR ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI})

include_directories(${INC_DIR})

add_library(ffmpeg SHARED IMPORTED)
set_target_properties(ffmpeg PROPERTIES IMPORTED_LOCATION ${LINK_DIR}/libffmpeg.so)

add_library(native-lib
             SHARED
             src/main/cpp/ffmpeg_push.cpp )

find_library(log-lib
              log )

target_link_libraries( native-lib
                       ffmpeg
                       ${log-lib} )
  • gradle
    修改gradle文件


    image.png
  1. 配置abiFilter
    因为我们只编译了这一种so文件
  2. 配置jniLibs
    需要把libffmpeg.so的文件配置成jniLibs的目标

代码

ffmpeg_push.cpp
代码的整体流程和思路和PC端一致。

#include <jni.h>
#include <string>
#include<android/log.h>
#include <exception>

//定义日志宏变量
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"ZZX",FORMAT,##__VA_ARGS__);
//#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"ZZX",FORMAT,##__VA_ARGS__);
#define logw(content)   __android_log_write(ANDROID_LOG_WARN,"eric",content)
#define loge(content)   __android_log_write(ANDROID_LOG_ERROR,"eric",content)
#define logd(content)   __android_log_write(ANDROID_LOG_DEBUG,"eric",content)


extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//引入时间
#include "libavutil/time.h"
}

#include <iostream>

using namespace std;

int avError(int errNum) {
    char buf[1024];
    //获取错误信息
    av_strerror(errNum, buf, sizeof(buf));
    loge(string().append("发生异常:").append(buf).c_str());
    return -1;
}



extern "C"
JNIEXPORT jstring JNICALL
Java_com_cry_ffmpegpushdemo_FFpush_getAvFormatInfo(JNIEnv *env, jobject instance) {

    char info[40000] = {0};
    av_register_all();
    AVInputFormat *in_temp = av_iformat_next(NULL);
    AVOutputFormat *out_temp = av_oformat_next(NULL);

    while (in_temp != NULL) {
        sprintf(info, " %s , Input: %s\n",info, in_temp->name);
//        logd("Input =", in_temp->name);
        in_temp = in_temp->next;
    }
    while (out_temp != NULL) {
        sprintf(info, "%s ,Output: %s\n", info,out_temp->name);
//        logd("Output =", out_temp->name);
        out_temp = out_temp->next;
    }

    return env->NewStringUTF(info);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_cry_ffmpegpushdemo_FFpush_push(JNIEnv *env, jobject instance, jstring fileName_,
                                        jstring pushUrl_) {
    const char *fileName = env->GetStringUTFChars(fileName_, 0);
    const char *pushUrl = env->GetStringUTFChars(pushUrl_, 0);

    int videoindex = -1;
    //所有代码执行之前要调用av_register_all和avformat_network_init
    //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
    av_register_all();

    //初始化网络库
    avformat_network_init();

    const char *inUrl = fileName;
    //输出的地址
    const char *outUrl = pushUrl;

    //////////////////////////////////////////////////////////////////
    //                   输入流处理部分
    /////////////////////////////////////////////////////////////////
    //打开文件,解封装 avformat_open_input
    //AVFormatContext **ps  输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
    //const char *url  路径
    //AVInputFormt * fmt 封装器
    //AVDictionary ** options 参数设置
    AVFormatContext *ictx = NULL;

    AVFormatContext *octx = NULL;

    AVPacket pkt;
    int ret = 0;
    try {
        //打开文件,解封文件头
        ret = avformat_open_input(&ictx, inUrl, 0, NULL);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avformat_open_input success!");
        //获取音频视频的信息 .h264 flv 没有头信息
        ret = avformat_find_stream_info(ictx, 0);
        if (ret != 0) {
            avError(ret);
            throw ret;
        }
        //打印视频视频信息
        //0打印所有  inUrl 打印时候显示,
        av_dump_format(ictx, 0, inUrl, 0);

        //////////////////////////////////////////////////////////////////
        //                   输出流处理部分
        /////////////////////////////////////////////////////////////////
        //如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
        //创建输出上下文
        ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avformat_alloc_output_context2 success!");

        int i;

        for (i = 0; i < ictx->nb_streams; i++) {

            //获取输入视频流
            AVStream *in_stream = ictx->streams[i];
            //为输出上下文添加音视频流(初始化一个音视频流容器)
            AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
            if (!out_stream) {
                printf("未能成功添加音视频流\n");
                ret = AVERROR_UNKNOWN;
            }
            if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
                out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
            }
            ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
            if (ret < 0) {
                printf("copy 编解码器上下文失败\n");
            }
            out_stream->codecpar->codec_tag = 0;
//        out_stream->codec->codec_tag = 0;
        }

        //找到视频流的位置
        for (i = 0; i < ictx->nb_streams; i++) {
            if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoindex = i;
                break;
            }
        }

        av_dump_format(octx, 0, outUrl, 1);
        //////////////////////////////////////////////////////////////////
        //                   准备推流
        /////////////////////////////////////////////////////////////////

        //打开IO
        ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avio_open success!");
        //写入头部信息
        ret = avformat_write_header(octx, 0);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avformat_write_header Success!");
        //推流每一帧数据
        //int64_t pts  [ pts*(num/den)  第几秒显示]
        //int64_t dts  解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)]  有了B帧压缩率更高。
        //获取当前的时间戳  微妙
        long long start_time = av_gettime();
        long long frame_index = 0;
        logd("start push >>>>>>>>>>>>>>>");
        while (1) {
            //输入输出视频流
            AVStream *in_stream, *out_stream;
            //获取解码前数据
            ret = av_read_frame(ictx, &pkt);
            if (ret < 0) {
                break;
            }

            /*
            PTS(Presentation Time Stamp)显示播放时间
            DTS(Decoding Time Stamp)解码时间
            */
            //没有显示时间(比如未解码的 H.264 )
            if (pkt.pts == AV_NOPTS_VALUE) {
                //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
                AVRational time_base1 = ictx->streams[videoindex]->time_base;

                //计算两帧之间的时间
                /*
                r_frame_rate 基流帧速率  (不是太懂)
                av_q2d 转化为double类型
                */
                int64_t calc_duration =
                        (double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);

                //配置参数
                pkt.pts = (double) (frame_index * calc_duration) /
                          (double) (av_q2d(time_base1) * AV_TIME_BASE);
                pkt.dts = pkt.pts;
                pkt.duration =
                        (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
            }

            //延时
            if (pkt.stream_index == videoindex) {
                AVRational time_base = ictx->streams[videoindex]->time_base;
                AVRational time_base_q = {1, AV_TIME_BASE};
                //计算视频播放时间
                int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                //计算实际视频的播放时间
                int64_t now_time = av_gettime() - start_time;

                AVRational avr = ictx->streams[videoindex]->time_base;
                cout << avr.num << " " << avr.den << "  " << pkt.dts << "  " << pkt.pts << "   "
                     << pts_time << endl;


                if (pts_time > now_time) {
                    //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                    av_usleep((unsigned int) (pts_time - now_time));
                }
            }

            in_stream = ictx->streams[pkt.stream_index];
            out_stream = octx->streams[pkt.stream_index];

            //计算延时后,重新指定时间戳
            pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                                       (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                                       (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,
                                              out_stream->time_base);
//        __android_log_print(ANDROID_LOG_WARN, "eric", "duration %d", pkt.duration);
            //字节流的位置,-1 表示不知道字节流位置
            pkt.pos = -1;

            if (pkt.stream_index == videoindex) {
                LOGI("Send %d video frames to output URL\n", frame_index);
                frame_index++;
            }
            //回调数据
//            callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
            //向输出上下文发送(向地址推送)
            ret = av_interleaved_write_frame(octx, &pkt);

            if (ret < 0) {
                printf("发送数据包出错\n");
                break;
            }
            //释放
            av_packet_unref(&pkt);
        }
        ret = 0;
    } catch (int errNum) {
    }
    logd("finish===============");
    //关闭输出上下文,这个很关键。
    if (octx != NULL)
        avio_close(octx->pb);
    //释放输出封装上下文
    if (octx != NULL)
        avformat_free_context(octx);
    //关闭输入上下文
    if (ictx != NULL)
        avformat_close_input(&ictx);
    octx = NULL;
    ictx = NULL;


    env->ReleaseStringUTFChars(fileName_, fileName);
    env->ReleaseStringUTFChars(pushUrl_, pushUrl);
}

参考

基于FFmpeg进行RTMP推流
最简单的基于FFmpeg的推流器(以推送RTMP为例)
FFMPEG中最关键的结构体之间的关系
FFMPEG结构体分析:AVPacket

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,640评论 0 3
  • FFmpeg 介绍 FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LG...
    Y了个J阅读 11,196评论 0 28
  • 简介 开发环境 FFmpeg sdk下载 项目配置 代码流程 开发环境 vs 2017 FFmpeg sdk下载 ...
    第八区阅读 31,180评论 7 23
  • ### YUV颜色空间 视频是由一帧一帧的数据连接而成,而一帧视频数据其实就是一张图片。 yuv是一种图片储存格式...
    天使君阅读 3,240评论 0 4
  • 我习惯了高傲,我习惯了优雅,我习惯了冷漠,然而始终有一群人,让我放弃了高傲,丢掉了优雅,告别了冷漠。 小时候没有手...
    大鹏_29wp阅读 343评论 1 5