1. 程式人生 > >Android 音視頻深入 十六 FFmpeg 推流手機攝像頭,實現直播 (附源碼下載)

Android 音視頻深入 十六 FFmpeg 推流手機攝像頭,實現直播 (附源碼下載)

音視頻 FFmpeg RTMP 直播 Android

源碼地址
https://github.com/979451341/RtmpCamera/tree/master

配置RMTP服務器,雖然之前說了,這裏就直接粘貼過來吧

1.配置RTMP服務器

這個我不多說貼兩個博客分別是在mac和windows環境上的,大家跟著弄
MAC搭建RTMP服務器
https://www.jianshu.com/p/6fcec3b9d644
這個是在windows上的,RTMP服務器搭建(crtmpserver和nginx)

https://www.jianshu.com/p/c71cc39f72ec
2.關於推流輸出的ip地址我好好說說

我這裏是手機開啟熱點,電腦連接手機,這個RTMP服務器的推流地址有localhost,服務器在電腦上,對於電腦這個localhost是127.0.0.1,但是對於外界比如手機,你不能用localhost,而是用這個電腦的在這個熱點也就是局域網的ip地址,不是127.0.0.1這個只代表本設備節點的ip地址,這個你需要去手機設置——》更多——》移動網絡共享——》便攜式WLAN熱點——》管理設備列表,就可以看到電腦的局域網ip地址了

3.代碼

我們這裏要用到SurfaceView和Camera這對老組合,多而不說,就是Camera的配置有的需要註意

                Camera.Parameters parameters = camera.getParameters();
                //對拍照參數進行設置
                for (Camera.Size size : parameters.getSupportedPictureSizes()) {
                    LogUtils.d(size.width + "  " + size.height);
                }

註意這段打印出來的寬高,後來設置Camera拍攝的圖片大小配置必須是裏面的一組,否則無法獲取Camera的回調數據,這個很關鍵

parameters.setPictureSize(screenWidth, screenHeight); // 設置照片的大小

還有cpp文件裏的寬高也要這樣,否則程序會崩潰,其實這裏的寬高我們可以通過比例縮放來處理,就可以任意使用寬高,但是我這裏沒有寫。。。。。。。。。

int width = 320;
int height = 240;

Camera預覽回調

camera.setPreviewCallback(new StreamIt()); // 設置回調的類

我們在這個回調裏傳送需要進行推流的數據,這裏通過isPlaying標識符控制了,需要我們點擊start按鈕才會開始推流,並且這裏傳輸數據的代碼是通過開啟一個單線程來完成,保證上次操作完成了才會執行下一次

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

    }
}

之前還執行了initVideo函數,初始化了FFmpeg並傳輸了推流地址

計算編碼出的yuv數據的大小

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號,這裏為264編碼器,可以根據video_st裏的codecID 參數賦值
pCodecCtx->codec_id = pCodec->id;
//像素的格式,也就是說采用什麽樣的色彩空間來表明一個像素點
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
//編碼器編碼的數據類型
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
//編碼目標的視頻幀大小,以像素為單位
pCodecCtx->width = width;
pCodecCtx->height = height;
pCodecCtx->framerate = (AVRational) {fps, 1};
//幀率的基本單位,我們用分數來表示,
pCodecCtx->time_base = (AVRational) {1, fps};
//目標的碼率,即采樣的碼率;顯然,采樣碼率越大,視頻大小越大
pCodecCtx->bit_rate = 400000;
//固定允許的碼率誤差,數值越大,視頻越小

// 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;
//最大和最小量化系數
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
//Optional Param
//兩個非B幀之間允許出現多少個B幀數
//設置0表示不使用B幀
//b 幀越多,圖片越小
pCodecCtx->max_b_frames = 0;

if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {

// av_dict_set(?m, "preset", "slow", 0);
/**

  • 這個非常重要,如果不設置延時非常的大
  • ultrafast,superfast, veryfast, faster, fast, medium
  • slow, slower, veryslow, placebo. 這是x264編碼速度的選項
    */
    av_dict_set(?m, "preset", "superfast", 0);
    av_dict_set(?m, "tune", "zerolatency", 0);
    }

打開編碼器

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

創建並配置一個視頻流

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);

查看輸出url是否有效,並根據輸出格式寫入文件頭

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);

接下來就是處理Camera傳送過來的數據

轉換數據格式

jbyte *in = env->GetByteArrayElements(buffer_, NULL);

根據編碼器獲取緩存圖片大小,並創建緩存圖片空間

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);

將之前創建的緩存圖片空間賦予AVFrame

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

轉換AVFrame格式,卓攝像頭數據為NV21格式,此處將其轉換為YUV420P格式

memcpy(pFrameYUV->data[0], in, y_length); //Y
pFrameYUV->pts = count;
for (int i = 0; i < uv_length; i++) {
    //將v數據存到第三個平面
    *(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
    //將U數據存到第二個平面
    *(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
}

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

編碼AVFrame數據

avcodec_send_frame(pCodecCtx, pFrameYUV);

獲取編碼後得到的數據

avcodec_receive_packet(pCodecCtx, &enc_pkt);

釋放AVFrame

av_frame_free(&pFrameYUV);

對編碼後的數據進行配置,設置播放時間等

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;

進行推流

av_interleaved_write_frame(ofmt_ctx, &enc_pkt);

釋放Camera傳輸過來的數據

env->ReleaseByteArrayElements(buffer_, in, 0);

最後釋放所有資源

if (video_st)
    avcodec_close(video_st->codec);
if (ofmt_ctx) {
    avio_close(ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);
    ofmt_ctx = NULL;
}

4.VLC的使用

在進行推流時,輸入推流地址,觀看推流數據,效果如下

Android 音視頻深入 十六 FFmpeg 推流手機攝像頭,實現直播 (附源碼下載)