前几天自身要谈的是财富管理的贰个特意的地点:流(streaming)。

   
随着多核CPU的遍布,四线程设计对3D引擎已经变得至关心器重大,很难想象一八年后生产的3D引擎还在使用单线程格局。不过二十四线程的引进使引擎变得更其千头万绪,不良的宏图带来的属性进步特别轻便,以至在单核意况下还有大概会油然则生明显的性质缩小。所以找到一种清晰、简洁、高效的企图形式就变得至关主要。前段时间四线程3D引擎安排的材质还相当少,许多只谈谈一些稳固的定义,缺少具体实例的分析。

正文转自:FFmpeg 入门(1):截取录像帧 |
www.samirchen.com

流的优点有二:

   
本文建议一种具体的3D引擎八线程达成方案,这种方案只是发端完结,还非常不够大批量的测量试验和深远运转的考验,但内部的思绪恐怕能够给我们提供一些参谋。近期风行的线程功效划分一般会把能源加载和图像渲染分别独立到贰个线程中,那也是这么些直观的划分格局,本文也不例外,也是使用这种线程功效的细分。

背景

在 Mac OS 上只要要运转教程中的相关代码供给先安装 FFmpeg,建议采纳 brew
来设置:

// 用 brew 安装 FFmpeg:
brew install ffmpeg

依然你能够参见在 Mac OS 上编译
FFmpeg运用源码编写翻译和安装
FFmpeg。

学科最早的文章地址:,本文中的代码做过局地纠正。

1、免除加载画面。

   
一种观念的线程模块设计艺术是将一组内聚性很强的做事分配给独立线程去做,如财富加载或图像展现,再为它设计一组命令新闻,主线程向职业线程提交命令消息,工作线程在后台完毕命令必要,再经过新闻将结果传递给主线程。这种布置存在八个难题,一是内需贯彻的一声令下音信多且复杂,同步困难;二是异步的命令实行和结果回传打破了主线程自然的运维逻辑,主线程不得不保留命令提交时的连带上下文,通过轮询或着回调函数的法子获取命令的实行结果,并结合保留的上下文最终完结命令的拍卖。本文介绍的安顿性力图回避上面提到的两点难点,简化命令消息设计,最佳只经过一种艺术交给命令央求,同有的时候候愿意完结央浼提交后不管,无需轮询和回调,纵然有也密闭在线程模块内部,对主线程透明。天下未有免费的中午举行的晚会,这种设计必然有它的受制,一是削减了职业线程的作用,只完毕单一、清晰的职责,将复杂的任务留给主线程,主线程的工作量非常大。二是能源加载线程必需合营一种能源加载预测算法,通过预读的主意以落成后台加载对主线程透明,对于盼望不经过预读就能够兑现能源后台加载的状态还索要主线程做轮询只怕响应回调。

概要

媒体文件经常有部分主干的组成都部队分。首先,文件本人被称作「容器(container)」,容器的类型定义了文本的新闻是哪些存款和储蓄,比如,AVI、QuickTime
等容器格式。接着,你须要通晓的概念是「流(streams)」,举个例子,你习感觉常会有伙同音频流和一同录制流。流中的数量成分被称呼「帧(frames)」。每路流都会被相应的「编/解码器(codec)」展开编码或解码(codec
那个名字正是发源 COded 和 DECoded)。codec
定义了实在多少是何许被编解码的,举例您用到的 codecs 恐怕是 DivX 和
DVD。「数据包(packets)」是从流中读取的数目片段,这么些数据片段中蕴藏的三个个比特正是解码后能最终被大家的应用程序管理的原始帧数据。为了完成大家音录像管理的靶子,每种数据包都满含着完全的帧,在点子情形下,二个数额包中只怕会含有四个音频帧。

依照以上这么些基础,管理录像流和音频流的经过实际上很轻便:

  • 1:从 video.avi 文件中开发 video_stream。
  • 2:从 video_stream 中读取数据包到 frame。
  • 3:如若数据包中的 frame 不完全,则跳到步骤 2。
  • 4:处理 frame。
  • 5:跳到步骤 2。

就算在部分顺序中上边步骤 4 管理 frame
的逻辑或许会特别复杂,可是在本文中的例程中,用 FFmpeg
来拍卖多媒体文件的一些会写的相比较轻便一些,这里大家将要做的正是开采二个媒体文件,读取在这之中的摄像流,将录像流中赢获得的录像帧写入到
PPM 文件中保存起来。

上面大家一步一步来贯彻。

2、突显了比内部存款和储蓄器能装入的还多的细节/连串。

    上边具体琢磨能源加载线程和制图线程的希图与特征。

开辟媒体文件

第一,我们来探视怎样开采媒体文件。在选用 FFmpeg 时,首先需求伊始化对应的
Library。

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
//...

int main(int argc, char *argv[]) {

    // Register all formats and codecs.
    av_register_all();

    // ...
}

上边的代码会登记 FFmpeg 库中兼有可用的「录制格式」和
「codec」,那样当使用库展开一个媒体文件时,就能够找到呼应的录像格式管理程序和
codec 来管理。需求潜心的是在运用 FFmpeg 时,你只需求调用
av_register_all() 二次就能够,因而大家在 main
中调用。当然,你也足以依照要求只登记给定的摄像格式和
codec,但平日你不要求那样做。

接下去我们将在早为之所开辟媒体文件了,那么媒体文件中有怎样音信是值得注意的呢?

  • 是不是含有:音频、录像。
  • 码流的封装格式,用于解封装。
  • 视频的编码格式,用于开端化录制解码器
  • 节奏的编码格式,用于开头化音频解码器。
  • 摄像的分辨率、帧率、码率,用于录制的渲染。
  • 节奏的采集样品率、位宽、通道数,用于伊始化音频播放器。
  • 码流的总时间长度,用于体现、拖动 Seek。
  • 别的 Metadata 消息,如小编、日期等,用于体现。

那么些注重的传媒音讯,被称作
metadata,平常记录在一切码流的上马大概结尾处,比方:wav 格式主要由
wav header 头来记录音频的采集样品率、通道数、位宽等重视新闻;VCD格式,则存放在 moov box 结构中;而 FLV 格式则记录在 onMetaData 中等等。

avformat_open_input
那个函数主要负担服务器的连天和码流头部消息的拉取,大家就用它来张开媒体文件:

AVFormatContext *pFormatCtx = NULL;

// Open video file.
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
    return -1; // Couldn't open file.
}

我们从程序入口获得要展开文件的门径,作为 avformat_open_input
函数的第二个参数字传送入,那几个函数会读取媒体文件的文本头并将文件格式相关的新闻存储在大家作为第三个参数字传送入的
AVFormatContext 数据结构中。avformat_open_input
函数的第多个参数用于钦命媒体文件格式,第八个参数是文件格式相关选项。借使您前边那多个参数字传送入的是
NULL,那么 libavformat 将机关探测文件格式。

接下去对于媒体新闻的探测和解析专业将在付出 avformat_find_stream_info
函数了:

// Retrieve stream information.
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
    return -1; // Couldn't find stream information.
}

avformat_find_stream_info 函数会为 pFormatCtx->streams
填充对应的新闻。这里还或者有二个调治用的函数 av_dump_format 可以为大家打字与印刷
pFormatCtx 中皆有怎么样音信。

// Dump information about file onto standard error.
av_dump_format(pFormatCtx, 0, argv[1], 0);

AVFormatContext 里包蕴了上面这么些跟媒体音信有关的积极分子:

  • struct AVInputFormat *iformat; // 记录了封装格式音讯
  • unsigned int nb_澳门太阳集团,streams; // 记录了该 U奥迪Q7L 中隐含有几路流
  • AVStream **streams; //
    二个结构体数组,各样对象记录了一路流的详细消息
  • int64_t start_time; // 第一帧的年月戳
  • int64_t duration; // 码流的总时间长度
  • int64_t bit_rate; // 码流的总码率,bps
  • AVDictionary *metadata; // 一些文书新闻头,key/value 字符串

您得到这么些数量后,与 av_dump_format
的输出相比较大概会发觉有的不等,那时候能够去拜会 FFmpeg 源码中
av_dump_format
的贯彻,里面前境遇打字与印刷出来的数目是有部分管理逻辑的。举个例子对于 start_time
的拍卖代码如下:

if (ic->start_time != AV_NOPTS_VALUE) {
    int secs, us;
    av_log(NULL, AV_LOG_INFO, ", start: ");
    secs = ic->start_time / AV_TIME_BASE;
    us = llabs(ic->start_time % AV_TIME_BASE);
    av_log(NULL, AV_LOG_INFO, "%d.%06d", secs, (int) av_rescale(us, 1000000, AV_TIME_BASE));
}

有鉴于此,经过 avformat_find_stream_info
的拍卖,大家能够获得媒体财富的封装格式、总时长、总码率了。别的
pFormatCtx->streams 是一个 AVStream
指针的数组,里面含有了媒体资源的每一路流新闻,数组的大大小小为
pFormatCtx->nb_streams

AVStream 结构体中注重的积极分子蕴涵:

  • AVCodecContext *codec; // 记录了该码流的编码音讯
  • int64_t start_time; // 第一帧的时间戳
  • int64_t duration; // 该码流的时间长度
  • int64_t nb_frames; // 该码流的总帧数
  • AVDictionary *metadata; // 一些文书音讯头,key/value 字符串
  • AVRational avg_frame_rate; // 平均帧率

此处能够得到平均帧率。

AVCodecContext 则记录了一路流的具体编码消息,当中最首要的积极分子满含:

  • const struct AVCodec *codec; // 编码的详细新闻
  • enum AVCodecID codec_id; // 编码类型
  • int bit_rate; // 平均码率
  • video only:
    • int width, height; //
      图像的宽高尺寸,码流中不自然存在该新闻,会由解码后覆盖
    • enum AVPixelFormat pix_fmt; //
      原始图像的格式,码流中不必然存在该音讯,会由解码后覆盖
  • audio only:
    • int sample_rate; // 音频的采集样品率
    • int channels; // 音频的通道数
    • enum AVSampleFormat sample_fmt; // 音频的格式,位宽
    • int frame_size; // 种种音频帧的 sample 个数

能够见到编码类型、图像的幅度中度、音频的参数都在此间了。

问询完那么些数据结构,我们随后往下走,直到大家找到一个录像流:

// Find the first video stream.
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
        videoStream = i;
        break;
    }
}
if (videoStream == -1) {
    return -1; // Didn't find a video stream.
}

// Get a pointer to the codec context for the video stream.
pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;

流信息中有关 codec 的一部分存款和储蓄在 codec context
中,这里包涵了那路流所接纳的具有的 codec
的消息,以后大家有二个对准它的指针了,不过大家随后还亟需找到真正的 codec
并张开它:

// Find the decoder for the video stream.
pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
if (pCodec == NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found.
}
// Copy context.
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context.
}

// Open codec.
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
    return -1; // Could not open codec.
}

亟待注意,大家不能够平昔利用录制流中的 AVCodecContext,所以我们要求用
avcodec_copy_context() 来拷贝一份新的 AVCodecContext 出来。

当年是二零一二年,假Norma文士的断言成真,那么这正是全数人被迫看加载画面包车型大巴末段一年。因为笔者从没直接参预游戏制作,而只是成立引擎,所以小编得以大胆地呶呶不休。

   
能源加载线程为制止复杂性,不做游戏对象的后台创立,只做游戏财富对象的创立。比如说正是不去创立代表主演恐怕NPC的模子对象,只创建立模型型对象所用到的mesh对象和贴图对象。况兼财富对象的创立分为两步,先是能源文件到内部存款和储蓄器数据块的加载,再是内部存款和储蓄器数据块的解析,创造最终的财富对象(包涵分配显存能源)。加载线程只完结财富创制的率先步,就是加载财富文件到内部存款和储蓄器,财富的解析和创设保留在主线程实现,但也属于加载模块的个中贯彻,对主线程的另外逻辑透明。那样做的目标就是最大化的简化加载线程的办事,纵然将数据深入分析和指标创立留给了主线程,但加载线程已经变成了花费最大的文书IO专门的学业,因此加载线程仍然分担了十分的大的专门的学问量,防止了主线程因为文件IO而围堵。加载线程不做财富对象的创始还应该有另一个指标,正是能源对象创设往往要分配显存财富,要和3D设备打交道,这就需求与图像彰显线程通讯、同步,上边的布署加载线程没有要求明白显示线程的存在,把与展示线程的一路难点交给主线程调控。上面让大家来探视加载线程的行事状态吧。主线程能够在其它时候提议加载央浼(注意,这里指预读央浼),其首要参数正是待加载文件的文书名。加载模块将呼吁放入三个待完结央浼的行列。加载线程不断从呼吁队列中抽出命令音信,完毕文件的开垦及读取,将加载后的内部存储器指针保存到命令音信结构中,并把成功后的通令音讯扩展到三个已产生命令的队列中去。主线程在每一帧的末尾会调用加载模块的三个后管理函数,这些函数从已成功命令的行列中收取每三个命令音信,完毕最后的财富分析和能源对象创建,并把创造的对象加入一个以能源文件名称为所引的哈希表中,以便前边查找。要专心的是这一步是在主线程中做到,所以不供给与主线程的别的一些做其余共同,因而得以轻便做到很复杂的操作。至于能源创建与显示线程的联合署宿就要末端的图样中看出。当主线程真正须求成立游戏对象的时候,游戏对象所信赖的财富对象已经做到了加载,所以游戏对象能够长足的创造出来。

积累数据

接下去,大家须要贰个地点来积存录像中的帧:

AVFrame *pFrame = NULL;

// Allocate video frame.
pFrame = av_frame_alloc();

由于我们安插将录制帧输出存款和储蓄为 PPM 文件,而 PPM 文件是会积存为 24-bit
EnclaveGB 格式的,所以咱们必要将录像帧从它自然的格式调换为 奥迪Q7GB。FFmpeg
可以帮我们做这么些。对于抢先50%的体系,大家大概都有将原本的录像帧转变为钦点格式的须要。现在大家就来创立叁个AVFrame
用于格式转变:

// Allocate an AVFrame structure.
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL) {
    return -1;
}

固然大家早就分配了内部存款和储蓄器类管理录制帧,当大家转格式时,咱们照样必要一块地点来积累录像帧的原始数据。我们应用
av_image_get_buffer_size 来获取必要的内部存储器大小,然后手动分配那块内部存款和储蓄器。

int numBytes;
uint8_t *buffer = NULL;

// Determine required buffer size and allocate buffer.
numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

av_malloc 是四个 FFmpeg 的 malloc,重借使对 malloc
做了一部分卷入来确认保障地址对齐之类的业务,它不会确定保证你的代码不发生内部存款和储蓄器泄漏、数十次释放或其它malloc 难点。

今后大家用 av_image_fill_arrays 函数来波及 frame
和大家刚刚分配的内部存储器。

// Assign appropriate parts of buffer to image planes in pFrameRGB Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);

后日,大家筹算从录像流读取数据了。

澳门太阳集团 1

   
再来看看绘制线程。一样为了简化绘制线程的繁杂,绘制线程不做显存能源的创建筑工程作,而把那一个职业留给主线程。那与数不清作品中提出的把对3d设备的享有操作都交给三个线程去做分裂等。绘制线程与主线程的通讯靠一组绘制上下文达成。在主线程的每叁个逻辑帧完毕后,主线程为每二个亟需绘制的靶子向绘制线程提交一份绘制上下文,这些绘制上下文包罗造成绘制职责所需的凡事音讯,如VB、IB、顶点注解、Shader以及具备Shader参数。有了那个上下文绘制线程就能够独自完结专门的学业,无需再和其他对象通讯了。在每一帧的伊始,主线程进行业下帧的逻辑运算,绘制线程同临时间对前一帧提交的绘图上下文做拍卖,绘制一帧镜头。当主线程完结了现阶段帧的逻辑运算,而绘制线程还未曾旗开得胜前一帧的绘图,恐怕绘制线程实现了绘图而主线程还从未到位逻辑计算时它们要相互等待,直到两个线程都实现了专门的学问。也正是说在每一帧的最终,主线程会和制图线程实行一次联合,在那个同步点后,绘制线程处于等候状态(阻塞),而主线程伊始调用加载模块的后管理函数,达成财富对象的开创(饱含显存财富的开创,注意这里绘制线程已经阻塞,不用再对3d设备的操作做联合了),然后主线程为当前帧提交一组绘制上下文。走到这里主线程通告绘制线程开始运作,绘制已经提交的上下文,同不经常间和睦也伊始新的一帧的逻辑管理。

读取数据

接下去大家要做的正是从整个摄像流中读取数据包
packet,并将数据解码到大家的 frame 中,一旦取得完全的
frame,我们就转变其格式并积攒它。

AVPacket packet;
int frameFinished;
struct SwsContext *sws_ctx = NULL;

// Initialize SWS context for software scaling.
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

// Read frames and save first five frames to disk.
i = 0;
while (av_read_frame(pFormatCtx, &packet) >= 0) {
    // Is this a packet from the video stream?
    if (packet.stream_index == videoStream) {
        // Decode video frame
        avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

        // Did we get a video frame?
        if (frameFinished) {
            // Convert the image from its native format to RGB.
            sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);

            // Save the frame to disk.
            if (++i <= 5) {
                SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
            }
        }
    }

    // Free the packet that was allocated by av_read_frame.
    av_packet_unref(&packet);
}

接下去的次第是比较好精晓的:av_read_frame()
函数从录像流中读取二个多少包 packet,把它存款和储蓄在 AVPacket
数据结构中。需求注意,大家只开创了 packet 结构,FFmpeg
则为我们填充了里面的数目,个中 packet.data
那么些指针会指向那个多少,而那一个多少占用的内存供给通过 av_packet_unref()
函数来刑释。avcodec_decode_video2() 函数将数据包 packet 转变为录像帧
frame。可是,我们大概不能够通过只解码贰个 packet 就收获贰个全部的摄像帧
frame,大概要求读取多个 packet 才行,avcodec_decode_video2()
会在解码到全体的一帧时设置 frameFinished
为真。最后当解码到完全的一帧时,我们用 sws_scale()
函数来将录像帧本来的格式 pCodecCtx->pix_fmt 转变为
普拉多GB。记住您能够将多少个 AVFrame 指针调换为三个 AVPicture
指针。最终,大家应用大家的 SaveFrame 函数来保存那个摄像帧到文件。

SaveFrame 函数中,我们将 LacrosseGB 信息写入到七个 PPM 文件中。

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
    FILE *pFile;
    char szFilename[32];
    int y;

    // Open file.
    sprintf(szFilename, "frame%d.ppm", iFrame);
    pFile = fopen(szFilename, "wb");
    if (pFile == NULL) {
        return;
    }

    // Write header.
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);

    // Write pixel data.
    for (y = 0; y < height; y++) {
        fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
    }

    // Close file.
    fclose(pFile);
}

上边大家回去 main
函数,当大家做到了录像流的读取,大家供给做一些实现职业:

// Free the RGB image.
av_free(buffer);
av_frame_free(&pFrameRGB);

// Free the YUV frame.
av_frame_free(&pFrame);

// Close the codecs.
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

// Close the video file.
avformat_close_input(&pFormatCtx);

return 0;

你能够见见,这里大家用 av_free() 函数来刑满释放解除劳教我们用 av_malloc()
分配的内部存款和储蓄器。

以上正是咱们那节教程的全部内容,当中的完整代码你可以从此间收获:

loading-screen(from gameaxis.com)

   
万语千言不及一副图片来得知道,最终贴上八个线程专门的职业的时序图作为最后。
澳门太阳集团 2

编写翻译实行

您能够选拔上面包车型地铁通令编译它:

$ gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lswscale -lz -lm

找三个媒体文件,你能够这么推行一下试试:

$ tutorial01 myvideofile.mp4

异常的快小结

在Bitsquid引擎中,资源(resource)是指依照名称和品种能一定识别的小块数据。比如:

类型        名称

unit       units/beings/player

texture    vegetation/grass/tall_grass_01

wav        music/soft_jazz

能源文件是由大家的工具创造的,是人类可读的公文,以JSON式的格式书写。在运作时刻内使用从前,财富文件必得通过编写翻译。数据编写翻译器将各类能源编译成平台湾特务定的优化后的二进制大对象(以下称为blob):

澳门太阳集团 3

streaming(from gamasutra)

为了便于装卸,财富被比物连类成数据包(package)。二个数据包(本身是能源)是一文山会海能源,可以同期装卸。

相关文章