Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(攝像頭推流)

簡(jiǎn)介

前面講到了在Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(視頻文件推流),里面主要是介紹如何解析視頻文件并進(jìn)行推流存皂,今天要給大家介紹如何在Android平臺(tái)下獲取采集的圖像软族,并進(jìn)行編碼推流画恰。同時(shí)項(xiàng)目工程也是在之前的代碼基礎(chǔ)上新增功能。源碼倉(cāng)庫(kù)地址FFmpegSample肛捍,這一節(jié)對(duì)應(yīng)的代碼版本是v1.2。大家注意不要下載錯(cuò)了版本之众。主要涉及的代碼拙毫。

QQ截圖20171124114855.png

建議:這套代碼和講解中,有些地方我也還沒研究透徹棺禾,但這個(gè)不影響我們要實(shí)現(xiàn)的功能缀蹄,我之前也特別糾結(jié)一些細(xì)節(jié),花了很多的時(shí)間。其實(shí)學(xué)習(xí)一門技術(shù)和框架是一個(gè)慢慢深入的過程缺前,剛開始我們先跑起來蛀醉,再深入,否則如果你還沒入門衅码,就開始糾結(jié)一些細(xì)節(jié)參數(shù)拯刁,然后又發(fā)現(xiàn)網(wǎng)上很難找到答案,那你的自信心就會(huì)受到打擊逝段,這也是我自己的體驗(yàn)垛玻,和大家分享一下。等到我們?cè)絹碓绞煜Fmpeg和一些技術(shù)奶躯,那么之前的問題都會(huì)迎刃而解

這套代碼我在4.4.2上運(yùn)行時(shí)沒問題的帚桩。所以如果有同學(xué)在5.0以上,如果涉及動(dòng)態(tài)權(quán)限問題嘹黔,大家加上即可朗儒。學(xué)習(xí)本章之前最好先看之前的文章,這里是一套連貫的教程

打開攝像頭并設(shè)置參數(shù)

具體代碼查看CameraActivity.java

    private Camera getCamera() {
        Camera camera;
        try {
            //打開相機(jī)参淹,默認(rèn)為后置醉锄,可以根據(jù)攝像頭ID來指定打開前置還是后置
            camera = Camera.open(1);
            if (camera != null && !isPreview) {
                try {
                    Camera.Parameters parameters = camera.getParameters();
                    //對(duì)拍照參數(shù)進(jìn)行設(shè)置
                    for (Camera.Size size : parameters.getSupportedPictureSizes()) {
                        LogUtils.d(size.width + "  " + size.height);
                    }
                    LogUtils.d("============");
                    for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
                        LogUtils.d(size.width + "  " + size.height);
                    }
                    parameters.setPreviewSize(screenWidth, screenHeight); // 設(shè)置預(yù)覽照片的大小
                    parameters.setPreviewFpsRange(30000, 30000);
                    parameters.setPictureFormat(ImageFormat.NV21); // 設(shè)置圖片格式
                    parameters.setPictureSize(screenWidth, screenHeight); // 設(shè)置照片的大小
                    camera.setParameters(parameters);
                    //指定使用哪個(gè)SurfaceView來顯示預(yù)覽圖片
                    camera.setPreviewDisplay(sv.getHolder()); // 通過SurfaceView顯示取景畫面
                    camera.setPreviewCallback(new StreamIt()); // 設(shè)置回調(diào)的類
                    camera.startPreview(); // 開始預(yù)覽
                    //Camera.takePicture()方法進(jìn)行拍照
                    camera.autoFocus(null); // 自動(dòng)對(duì)焦
                } catch (Exception e) {
                    e.printStackTrace();
                }
                isPreview = true;
            }
        } catch (Exception e) {
            camera = null;
            e.printStackTrace();
            Toast.makeText(this, "無法獲取前置攝像頭", Toast.LENGTH_LONG);
        }
        return camera;
    }

Camera.open(int cameraId)

這里是創(chuàng)建一個(gè)Camera對(duì)象對(duì)應(yīng)具體的硬件攝像頭,如果攝像頭已經(jīng)被其他app打開浙值,就會(huì)拋出RuntimeException異常恳不。

cameraId是camera的Id。我們可以通過getNumberOfCameras()

獲取攝像頭的數(shù)量开呐,那id的范圍就是0~(getNumberOfCameras()-1)烟勋。一般情況下傳0就直接獲取到后置攝像頭,1就獲取到前置攝像頭筐付。當(dāng)然有些設(shè)備可能有些不同卵惦。

Camera.Parameters

這個(gè)類用于存儲(chǔ)和設(shè)置攝像頭的參數(shù)信息,當(dāng)然Camera有很多默認(rèn)參數(shù)瓦戚,所以我們只需要通過camera.getParameters()獲取該對(duì)象沮尿,然后并設(shè)置我們需要修改的屬性即可。我們看一些常見的屬性設(shè)置

  • setPreviewSize

    設(shè)置預(yù)覽圖像的大小

  • setPictureSize

    設(shè)置照片的大小

  • setPreviewFpsRange

    設(shè)置Fps较解,幀率畜疾。但我發(fā)現(xiàn)并沒有什么卵用。每次修改后采集的頻率還是沒變印衔,擦啡捶!

  • setPictureFormat

    設(shè)置采集到圖像的像素格式,Android推薦NV21奸焙。那我們就用這個(gè)瞎暑,這個(gè)參數(shù)很重要彤敛,后面編碼我們會(huì)詳細(xì)講解。

最后不要忘了調(diào)用setParameters進(jìn)行設(shè)置了赌。否則你就白忙活了墨榄。

預(yù)覽和獲取采集圖像數(shù)據(jù)

預(yù)覽

第一個(gè)問題,用什么來承載預(yù)覽圖像揍拆。Android提供了SurfaceView和GLSurfaceView渠概。這里為了方便大家上手茶凳,我們先選擇使用SurfaceView稍微簡(jiǎn)單一點(diǎn)嫂拴,對(duì)SurfaceView大家不熟的可以查找相關(guān)資料。接下來就是使用SurfaceView

  • 布局中添加SurfaceView贮喧。這里我做了一個(gè)繼承類MySurfaceView

        <com.wangheart.rtmpfile.MySurfaceView
            android:id="@+id/sv"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
  • 獲取SurfaceHolder并設(shè)置回調(diào)

    SurfaceView里有一個(gè)SurfaceHolder用來控制SurfaceView的相關(guān)操作筒狠。比如設(shè)置SurfaceView的Callback,用來監(jiān)聽SurfaceView的創(chuàng)建箱沦,變化和銷毀辩恼。這里只需要實(shí)現(xiàn)SurfaceHolder.Callback的接口

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            setStartPreview(mCamera, mHolder);
        }
    
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            setStartPreview(mCamera, mHolder);
        }
    
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            releaseCamera();
        }
    

    然后設(shè)置到SurfaceHolder中mHolder.addCallback(this)

  • SurfaceView與Camera關(guān)聯(lián)

    因?yàn)槲覀円v圖像預(yù)覽到SurfaceView上,那么必定有地方存在關(guān)聯(lián)谓形。這里很簡(jiǎn)單灶伊,就是調(diào)用Camera的setPreviewDisplay,將SurfaceView的SurfaceHolder設(shè)置進(jìn)去即可寒跳。

  • 開始預(yù)覽

    直接調(diào)用camera的startPreview開始進(jìn)行預(yù)覽聘萨。那么什么時(shí)候調(diào)用這個(gè)方法呢?

    1. 設(shè)置一個(gè)按鈕童太,點(diǎn)擊之后我們就調(diào)用這個(gè)方法進(jìn)行預(yù)覽
    2. SurfaceView的創(chuàng)建回調(diào)方法中surfaceCreated中進(jìn)行調(diào)用米辐,因?yàn)閳D像要預(yù)覽到SurfaceView中,所以必須得SurfaceView已成功創(chuàng)建书释。

獲取采集數(shù)據(jù)

前面我們已經(jīng)知道怎么預(yù)覽圖像了翘贮。接下來就是獲取采集數(shù)據(jù)。這個(gè)也很容易就是調(diào)用Camera的setPreviewCallback設(shè)置預(yù)覽回調(diào)爆惧。我們實(shí)現(xiàn)一下這個(gè)接口

    public class StreamIt implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(final byte[] data, Camera camera) {
            long endTime = System.currentTimeMillis();
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    encodeTime = System.currentTimeMillis();
                    FFmpegHandle.getInstance().onFrameCallback(data);
                    LogUtils.w("編碼第:" + (encodeCount++) + "幀狸页,耗時(shí):" + (System.currentTimeMillis() - encodeTime));
                }
            });
            LogUtils.d("采集第:" + (++count) + "幀,距上一幀間隔時(shí)間:"
                    + (endTime - previewTime) + "  " + Thread.currentThread().getName());
            previewTime = endTime;
        }
    }

很簡(jiǎn)單扯再,這個(gè)接口就是講原始數(shù)據(jù)進(jìn)行回調(diào)肴捉。這里大家也看到了,我把采集的時(shí)間間隔和編碼消耗的時(shí)間打印出來了叔收。

編碼

前面把基礎(chǔ)的如何采集攝像頭數(shù)據(jù)講了一下齿穗,接下來就是進(jìn)行視頻數(shù)據(jù)編碼。

開啟線程編碼

因?yàn)榫幋a畢竟會(huì)比較耗時(shí)饺律,所以我們需要放到線程中處理窃页,這里我用了一個(gè)單線程池,避免每次開啟和銷毀線程產(chǎn)生的開銷。為了保證圖片按順序編碼脖卖,這里使用單線程池乒省。

ExecutorService executor = Executors.newSingleThreadExecutor();

獲取到采集的數(shù)據(jù)后就可以丟進(jìn)去進(jìn)行編碼

executor.execute(new Runnable() {
    @Override
    public void run() {
        encodeTime = System.currentTimeMillis();
        FFmpegHandle.getInstance().onFrameCallback(data);
        LogUtils.w("編碼第:" + (encodeCount++) + "幀,耗時(shí):" + (System.currentTimeMillis() - encodeTime));
    }
});

這里大家也看出來了調(diào)用FFmpegHandle.getInstance().onFrameCallback(data);進(jìn)行編碼畦木。

初始化編碼相關(guān)操作

這里我們使用的是FFmpeg,所以在編碼前我們會(huì)先做一些初始化以及參數(shù)設(shè)置工作袖扛,所以我們?cè)贔FmpegHandle中增加一個(gè)native方法public native int initVideo(String url);

對(duì)應(yīng)到C++層,也就是ffmpeg_handle.cpp

AVFormatContext *ofmt_ctx;
AVStream *video_st;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVPacket enc_pkt;
AVFrame *pFrameYUV;
int count = 0;
int yuv_width;
int yuv_height;
int y_length;
int uv_length;
int width = 480;
int height = 320;
int fps = 15;
/**
 * 初始化
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_initVideo(JNIEnv *env, jobject instance,
                                                          jstring url_) {
    const char *out_path = env->GetStringUTFChars(url_, 0);
    logd(out_path);

    //計(jì)算yuv數(shù)據(jù)的長(zhǎng)度
    yuv_width = width;
    yuv_height = height;
    y_length = width * height;
    uv_length = width * height / 4;

    av_register_all();

    //output initialize
    avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);
    //output encoder initialize
    pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!pCodec) {
        loge("Can not find encoder!\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec);
    //編碼器的ID號(hào)十籍,這里為264編碼器蛆封,可以根據(jù)video_st里的codecID 參數(shù)賦值
    pCodecCtx->codec_id = pCodec->id;
    //像素的格式,也就是說采用什么樣的色彩空間來表明一個(gè)像素點(diǎn)
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    //編碼器編碼的數(shù)據(jù)類型
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    //編碼目標(biāo)的視頻幀大小勾栗,以像素為單位
    pCodecCtx->width = width;
    pCodecCtx->height = height;
    pCodecCtx->framerate = (AVRational) {fps, 1};
    //幀率的基本單位惨篱,我們用分?jǐn)?shù)來表示,
    pCodecCtx->time_base = (AVRational) {1, fps};
    //目標(biāo)的碼率围俘,即采樣的碼率砸讳;顯然,采樣碼率越大界牡,視頻大小越大
    pCodecCtx->bit_rate = 400000;
    //固定允許的碼率誤差簿寂,數(shù)值越大,視頻越小
//    pCodecCtx->bit_rate_tolerance = 4000000;
    pCodecCtx->gop_size = 50;
    /* Some formats want stream headers to be separate. */
    if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
        pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;

    //H264 codec param
//    pCodecCtx->me_range = 16;
    //pCodecCtx->max_qdiff = 4;
    pCodecCtx->qcompress = 0.6;
    //最大和最小量化系數(shù)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    //Optional Param
    //兩個(gè)非B幀之間允許出現(xiàn)多少個(gè)B幀數(shù)
    //設(shè)置0表示不使用B幀
    //b 幀越多宿亡,圖片越小
    pCodecCtx->max_b_frames = 0;
    // Set H264 preset and tune
    AVDictionary *param = 0;
    //H.264
    if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
//        av_dict_set(&param, "preset", "slow", 0);
        /**
         * 這個(gè)非常重要常遂,如果不設(shè)置延時(shí)非常的大
         * ultrafast,superfast, veryfast, faster, fast, medium
         * slow, slower, veryslow, placebo. 這是x264編碼速度的選項(xiàng)
       */
        av_dict_set(&param, "preset", "superfast", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }

    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) {
        loge("Failed to open encoder!\n");
        return -1;
    }

    //Add a new stream to output,should be called by the user before avformat_write_header() for muxing
    video_st = avformat_new_stream(ofmt_ctx, pCodec);
    if (video_st == NULL) {
        return -1;
    }
    video_st->time_base.num = 1;
    video_st->time_base.den = fps;
//    video_st->codec = pCodecCtx;
    video_st->codecpar->codec_tag = 0;
    avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);

    //Open output URL,set before avformat_write_header() for muxing
    if (avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE) < 0) {
        loge("Failed to open output file!\n");
        return -1;
    }

    //Write File Header
    avformat_write_header(ofmt_ctx, NULL);

    return 0;
}

首先需要聲明一些全局的變量,方便后面編碼使用AVFormatContext她混、AVStream等烈钞。

  • 進(jìn)行FFmpeg初始化

    這個(gè)和之前講到的一樣av_register_all()

  • 創(chuàng)建輸出格式上下文

    avformat_alloc_output_context2這些之前都講到過,就不錯(cuò)累述

  • 獲取編碼器

    pCodec = avcodec_find_encoder(AV_CODEC_ID_H264)獲取編碼器坤按。這里我們使用H264進(jìn)行視頻編碼毯欣。如果編碼器獲取失敗就沒有下文了

  • 創(chuàng)建編碼器上下文

    pCodecCtx = avcodec_alloc_context3(pCodec)

  • 設(shè)置編碼器參數(shù)

    這些參數(shù)我個(gè)參數(shù)的設(shè)置上都有加注釋,大家查看即可臭脓。同時(shí)參考音視頻編碼相關(guān)名詞詳解酗钞。這講幾個(gè)主要的參數(shù)

    • pix_fmt 像素的格式這里我們使用的AV_PIX_FMT_YUV420P,也就是YUV平面格式来累,三個(gè)平面分別存放Y砚作、U、V數(shù)據(jù)嘹锁。
    • codec_type 編碼器編碼的數(shù)據(jù)類型
    • framerate 幀率
    • time_base 幀率的基本單位
    • gop_size GOP的大小
  • AVDictionary設(shè)置

    • 前面講了一些常規(guī)參數(shù)的設(shè)置葫录,這里還有一些重要參數(shù)設(shè)置

          if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
      //        av_dict_set(&param, "preset", "slow", 0);
              /**
               * 這個(gè)非常重要,如果不設(shè)置延時(shí)非常的大
               * ultrafast,superfast, veryfast, faster, fast, medium
               * slow, slower, veryslow, placebo. 這是x264編碼速度的選項(xiàng)
             */
              av_dict_set(&param, "preset", "superfast", 0);
              av_dict_set(&param, "tune", "zerolatency", 0);
          }
      

      一定要注意preset這個(gè)參數(shù)的設(shè)置领猾。否則你會(huì)發(fā)現(xiàn)你編碼的延遲特別大米同,網(wǎng)上有許多朋友遇到這個(gè)問題骇扇。

  • 使用給定的編碼器和參數(shù)初始化編碼上下文

    avcodec_open2(pCodecCtx, pCodec, &param)

  • 創(chuàng)建視頻流

    video_st = avformat_new_stream(ofmt_ctx, pCodec)這個(gè)就和之前的推文件流一樣了。創(chuàng)建并設(shè)置相關(guān)的參數(shù)

  • 打開輸出上下文

    avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE)

  • 寫入輸出頭信息

    avformat_write_header(ofmt_ctx, NULL)

開始編碼

在獲取到采集的時(shí)候后我們通過線程池調(diào)用執(zhí)行了FFmpegHandle.getInstance().onFrameCallback(mData);接下來我們重點(diǎn)看到onFrameCallback方法面粮。當(dāng)然這也是一個(gè)navive方法少孝。我們看到c++層的實(shí)現(xiàn)。

Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_onFrameCallback(JNIEnv *env, jobject instance,
                                                                jbyteArray buffer_) {
//    startTime = av_gettime();
    jbyte *in = env->GetByteArrayElements(buffer_, NULL);

    int ret = 0;

    pFrameYUV = av_frame_alloc();
    int picture_size = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width,
                                                pCodecCtx->height, 1);
    uint8_t *buffers = (uint8_t *) av_malloc(picture_size);


    //將buffers的地址賦給AVFrame中的圖像數(shù)據(jù)熬苍,根據(jù)像素格式判斷有幾個(gè)數(shù)據(jù)指針
    av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, pCodecCtx->pix_fmt,
                         pCodecCtx->width, pCodecCtx->height, 1);

    //安卓攝像頭數(shù)據(jù)為NV21格式稍走,此處將其轉(zhuǎn)換為YUV420P格式
    ////N21   0~width * height是Y分量,  width*height~ width*height*3/2是VU交替存儲(chǔ)
    //復(fù)制Y分量的數(shù)據(jù)
    memcpy(pFrameYUV->data[0], in, y_length); //Y
    pFrameYUV->pts = count;
    for (int i = 0; i < uv_length; i++) {
        //將v數(shù)據(jù)存到第三個(gè)平面
        *(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
        //將U數(shù)據(jù)存到第二個(gè)平面
        *(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
    }

    pFrameYUV->format = AV_PIX_FMT_YUV420P;
    pFrameYUV->width = yuv_width;
    pFrameYUV->height = yuv_height;

    //例如對(duì)于H.264來說柴底。1個(gè)AVPacket的data通常對(duì)應(yīng)一個(gè)NAL
    //初始化AVPacket
    av_init_packet(&enc_pkt);
//    __android_log_print(ANDROID_LOG_WARN, "eric", "編碼前時(shí)間:%lld",
//                        (long long) ((av_gettime() - startTime) / 1000));
    //開始編碼YUV數(shù)據(jù)
    ret = avcodec_send_frame(pCodecCtx, pFrameYUV);
    if (ret != 0) {
        logw("avcodec_send_frame error");
        return -1;
    }
    //獲取編碼后的數(shù)據(jù)
    ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);
//    __android_log_print(ANDROID_LOG_WARN, "eric", "編碼時(shí)間:%lld",
//                        (long long) ((av_gettime() - startTime) / 1000));
    //是否編碼前的YUV數(shù)據(jù)
    av_frame_free(&pFrameYUV);
    if (ret != 0 || enc_pkt.size <= 0) {
        loge("avcodec_receive_packet error");
        avError(ret);
        return -2;
    }
    enc_pkt.stream_index = video_st->index;
    AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };
    enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);
    enc_pkt.dts = enc_pkt.pts;
    enc_pkt.duration = (video_st->time_base.den) / ((video_st->time_base.num) * fps);
    __android_log_print(ANDROID_LOG_WARN, "eric",
                        "index:%d,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d",
                        count,
                        (long long) enc_pkt.pts,
                        (long long) enc_pkt.dts,
                        (long long) enc_pkt.duration,
                        time_base.num, time_base.den);
    enc_pkt.pos = -1;
//    AVRational time_base_q = {1, AV_TIME_BASE};
//    //計(jì)算視頻播放時(shí)間
//    int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
//    //計(jì)算實(shí)際視頻的播放時(shí)間
//    if (count == 0) {
//        startTime = av_gettime();
//    }
//    int64_t now_time = av_gettime() - startTime;
//    __android_log_print(ANDROID_LOG_WARN, "eric", "delt time :%lld", (pts_time - now_time));
//    if (pts_time > now_time) {
//        //睡眠一段時(shí)間(目的是讓當(dāng)前視頻記錄的播放時(shí)間與實(shí)際時(shí)間同步)
//        av_usleep((unsigned int) (pts_time - now_time));
//    }

    ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
    if (ret != 0) {
        loge("av_interleaved_write_frame failed");
    }
    count++;
    env->ReleaseByteArrayElements(buffer_, in, 0);
    return 0;

}

像素格式轉(zhuǎn)換

在設(shè)置攝像頭采集的圖像格式時(shí)候我們?cè)O(shè)置的是NV21婿脸。而我們編碼需要的是AV_PIX_FMT_YUV420P。所以這需要進(jìn)行轉(zhuǎn)換似枕。我們先看下兩個(gè)像素格式的區(qū)別

  • NV21

    是一個(gè)YUV 4:2:0數(shù)據(jù)盖淡,應(yīng)該說是平面和打包混合存儲(chǔ)年柠。有兩個(gè)平面凿歼,第一個(gè)平面存放Y數(shù)據(jù)第二個(gè)平面VU數(shù)據(jù)交替存儲(chǔ)

  • AV_PIX_FMT_YUV420P

    那不用說是我們熟悉的YUV 4:2:0的像素?cái)?shù)據(jù),它是純平面存儲(chǔ)冗恨〈疸荆總共三個(gè)平面,分別存放掀抹,Y虐拓、U、V數(shù)據(jù)傲武。

我們還需要了解蓉驹,以為采集的數(shù)據(jù)YUV是4:2:0。所以Y:(U或V)的大小是4:1揪利。而U:V是1:1态兴。所以當(dāng)圖像寬是width,高是height時(shí)疟位,Y分量的大小就是width×heitht,而U是width×heitht/4,V也是U是width×heitht/4瞻润。

知道上面的存儲(chǔ)格式后我們就知道怎么轉(zhuǎn)換了。

首先復(fù)制Y分量的數(shù)據(jù)

memcpy(pFrameYUV->data[0], in, y_length);

然后遍歷VU數(shù)據(jù)并存放到data[1]和data[2]平面中

    for (int i = 0; i < uv_length; i++) {
        //將v數(shù)據(jù)存到第三個(gè)平面
        *(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
        //將U數(shù)據(jù)存到第二個(gè)平面
        *(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
    }

H264編碼

首先我們需要了解兩個(gè)數(shù)據(jù)結(jié)構(gòu)AVFrame甜刻、AVPacket

AVFrame存放的是原始數(shù)據(jù)绍撞、AVPacket存放的是編碼后的數(shù)據(jù)。所以前面格式轉(zhuǎn)換也是將數(shù)據(jù)存放到pFrameYUV中得院。

  • 初始化AVPacket

    av_init_packet(&enc_pkt);

  • 開始編碼

    ret = avcodec_send_frame(pCodecCtx, pFrameYUV);

  • 接受編碼后的數(shù)據(jù)

    ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);

是不是很簡(jiǎn)單傻铣,這樣編碼后的數(shù)據(jù)就存到了enc_pkt中。到這里只是完成的編碼工作祥绞,接下來還有一些參數(shù)需要設(shè)置

PTS非洲、DTS阱驾、duration

PTS是顯示時(shí)間戳,DTS解碼時(shí)間戳怪蔑,duration是當(dāng)當(dāng)前幀和下一幀的時(shí)間間隔,里覆。這個(gè)很重要,不然播放會(huì)出現(xiàn)問題缆瓣。

首先我們要知道時(shí)間基數(shù)喧枷,也就是你按什么時(shí)間單位算。

AVRational time_base = ofmt_ctx->streams[0]->time_base;

這里的值是{1,1000}弓坞,這應(yīng)該就是毫秒隧甚。知道時(shí)間基礎(chǔ),同時(shí)根據(jù)fps我們就知道每一幀的時(shí)間間隔是1000/fps渡冻。

那第n幀的pts就是n×(1000/fps)戚扳。對(duì)應(yīng)代碼

enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);

dts和pts設(shè)置成一樣,標(biāo)示解碼時(shí)間和顯示時(shí)間一致族吻。至于為什么帽借,其實(shí)我也沒太明白,如果不一致該怎么計(jì)算超歌?我們先不管砍艾,以后再研究。

duration那就容易了巍举,就是(video_st->time_base.den) / ((video_st->time_base.num) * fps)

這里的三個(gè)參數(shù)這是可能不太準(zhǔn)確脆荷,但我們先這樣,想把功能跑起來再說懊悯,不然我們糾結(jié)這些就永無止境了蜓谋。后面等我們深入了,也就會(huì)明白炭分。

輸出視頻數(shù)據(jù)

ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);

釋放資源

在結(jié)束編碼推流后我們也需要釋放相關(guān)的資源

    if (video_st)
        avcodec_close(video_st->codec);
    if (ofmt_ctx) {
        avio_close(ofmt_ctx->pb);
        avformat_free_context(ofmt_ctx);
        ofmt_ctx = NULL;
    }
    return 0;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桃焕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子欠窒,更是在濱河造成了極大的恐慌覆旭,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岖妄,死亡現(xiàn)場(chǎng)離奇詭異型将,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)荐虐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門七兜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人福扬,你說我怎么就攤上這事腕铸∠” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵狠裹,是天一觀的道長(zhǎng)虽界。 經(jīng)常有香客問我,道長(zhǎng)涛菠,這世上最難降的妖魔是什么莉御? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮俗冻,結(jié)果婚禮上礁叔,老公的妹妹穿的比我還像新娘。我一直安慰自己迄薄,他們只是感情好琅关,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著讥蔽,像睡著了一般涣易。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勤篮,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天都毒,我揣著相機(jī)與錄音色罚,去河邊找鬼碰缔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛戳护,可吹牛的內(nèi)容都是我干的金抡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼腌且,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼梗肝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铺董,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤巫击,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后精续,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坝锰,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年重付,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了顷级。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡确垫,死狀恐怖弓颈,靈堂內(nèi)的尸體忽然破棺而出帽芽,到底是詐尸還是另有隱情,我是刑警寧澤翔冀,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布导街,位于F島的核電站,受9級(jí)特大地震影響纤子,放射性物質(zhì)發(fā)生泄漏菊匿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一计福、第九天 我趴在偏房一處隱蔽的房頂上張望跌捆。 院中可真熱鬧,春花似錦象颖、人聲如沸佩厚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抄瓦。三九已至,卻和暖如春陶冷,著一層夾襖步出監(jiān)牢的瞬間钙姊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工埂伦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留煞额,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓沾谜,卻偏偏與公主長(zhǎng)得像膊毁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子基跑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359