最純粹的直播技術實戰02-Camera的處理以及推流
最新實戰教程,Android自動化刷量、作弊與防作弊,案例:刷友盟統計、批量註冊蘋果帳號
這個系列的文章將會研究最純粹的Android直播的實現,而且不是用現在的集成SDK來達到直播的技術實現,而是從一個比較底層的直播實現來探討這個技術,這樣子對於直播技術的實現,現成的一些直播框架等都有一個比較好的理解。
上一篇文章裏面,我們完成了FFmpeg的編譯,然後也把編譯出來的庫運行在了Android上,那接下來就要處理Android的Camera以及推流的實現了。如果沒有看過上一篇文章的可以戳這裏
我們會使用上一篇文章那工程項目來繼續後續的功能編寫,產生,我們在MainActivity裏面添加兩個Button,一個是直播的,一個是看直播的
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.xiaoxiao.live.MainActivity">
<TextView
android:id="@+id/main_tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="Hello World!" />
<Button
android:id="@+id/main_bt_live"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我要直播" />
<Button
android:id="@+id/main_bt_watch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="看直播" />
</LinearLayout>
layout完成之後呢,就去MainActivity裏面處理一下Button的點擊操作了
我們把上一次的測試TextView註釋掉了,然後新建了一個LiveActivity來處理Camera以及推流,主要就是展示直播。
後續還會有看直播的處理,就要就是拉流以及視頻的播放了
接下來就要在LiveActivity裏面處理一下Camera的東西了,首先要在LiveActivity的layout裏面添加一個SurfaceView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/live_sv_live"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
因爲不是拍照,所以Camera的處理就會顯得比較的簡單了,
package com.xiaoxiao.live;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* Created by Administrator on 2017/2/20.
*/
public class LiveActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
private Camera mCamera;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private int mCameraId = 0;
private int width = 720;
private int height = 480;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live);
mSurfaceView = (SurfaceView) findViewById(R.id.live_sv_live);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.setFixedSize(width, height);
mSurfaceHolder.addCallback(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_live, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.checkable_menu) {
boolean isChecked = item.isChecked();
Log.e("LiveActivity", "checked: " + isChecked);
item.setChecked(!isChecked);
mCameraId = 1 - mCameraId;
destroyCamera();
initCamera();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
initCamera();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
destroyCamera();
}
private void initCamera() {
try {
mCamera = Camera.open(mCameraId);
mCamera.setPreviewDisplay(mSurfaceHolder);
Camera.Parameters params = mCamera.getParameters();
//設置預覽大小
params.setPreviewSize(width, height);
//設置生成的照片大小
params.setPictureSize(width, height);
params.setPreviewFormat(ImageFormat.NV21);
mCamera.setDisplayOrientation(90);
//params.setRotation(90);
/*List<Camera.Size> sizes = params.getSupportedPreviewSizes();
for(Camera.Size s : sizes) {
Log.e("LiveActivity", s.width + " X " + s.height);
}*/
mCamera.setParameters(params);
mCamera.setPreviewCallback(this);
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
private void destroyCamera() {
if(mCamera == null) {
return;
}
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
我們通過menu來做了一個攝像頭的切換功能,這樣子就可以前攝像頭直播或者後攝像頭直播了。
到時會在onPreviewFrame裏面獲取到數據,然後交給jni進行一個編碼的處理,然後就推流
那麼這裏就會有一個非常重要的知識點了:
我們通過setPreviewFormat方法把預覽的數據(onPreviewFrame方法參數裏面的data)的格式設置成了ImageFormat.NV21,一般來說,常用的格式是NV21或者YV12,因爲這兩種格式被所有的攝像頭支持的,Android默認是會設置NV21的。
那麼什麼是NV21或YV12呢,其實這也是一種yuv格式的數據來的
上一篇文章我們已經說過了,就是通過把yuv通過編碼,然後再封裝就可以得到一個視頻文件了,但我們還需要對這種yuv進行一定的處理,因爲yuv也是有不同的各類的。
yuv通常分成兩大格式,一種是planar:把所有像素點的Y值全部存放在數組的最前面,然後再存放所有像素點的U值,最後再存放所有像素點的V值
還有一種就是packed:它是依次存放每一個像素點的YUV值的
同時yuv還有不同的採樣方式,一般主流的有三種:
- YUV4:4:4 每一個Y對應一組UV分量
- YUV4:2:2 每兩個Y共用一組UV分量
- YUV4:2:0 每四個Y共用一組UV分量
假設一張720 X 480的圖片存儲成yuv格式:
- YUV4:4:4 Y = 720 * 480 U = V = 720 * 480 所以整個數組的大小就是720 * 480 * 3
- YUV4:2:2 Y = 720 * 480 U = V = 720 * 480 / 2 所以整個數組的大小就是720 * 480 * 2
- YUV4:2:0 Y = 720 * 480 U = V = 720 * 480 / 4 所以整個數組的大小就是720 * 480 * 1.5
NV21和YV12就是YUV4:2:0這種採樣格式的,而且我們到時用FFmpeg編碼採用的格式一般是AV_PIX_FMT_YUV420P,都是YUV4:2:0這種採樣格式的
但還是有一些差別的
- AV_PIX_FMT_YUV420P 格式是planar,就是先存全部的Y再存全部的U再存全部的V,採樣格式4:2:0。存儲格式類似 yyyyyyyy uu vv 這樣
- NV21 格式也是planar,採樣格式也是4:2:0。存儲格式類似 yyyyyyyy vu vu
- YV12 格式也是planar,採樣格式也是4:2:0。存儲格式類似 yyyyyyyy vv uu
從上面可以看到,我們需要用的格式和預覽的格式還是有些差別的,所以我們到時要處理一下。
那麼現在我們可以先把我們的Camera的功能給測試一下先的,看看能不能預覽成功,但在運行前,還需要去AndroidManifest裏面配置一下
如果Camera模塊測試沒有問題的話,我們就可以來寫native方法了,首先在LiveActivity裏面定義好幾個native方法
/**
* 初始化編碼的一些東西,比如編碼器等
* @param width 編碼視頻的寬
* @param height 編碼視頻的高
* @return 0 成功 小於0失敗
*/
private native int streamerInit(int width, int height);
/**
* 對每一次預覽的數據進行編碼推流
* @param data NV21格式的數據
* @return 0成功,小於0失敗
*/
private native int streamerHandle(byte[] data);
/**
* 把緩衝幀的數據清空
* @return 0成功,小於0失敗
*/
private native int streamerFlush();
/**
* 釋放資源,比如編碼器這些
* @return 0成功,小於0失敗
*/
private native int streamerRelease();
定義完成native方法後,我們先把LiveActivity裏面的邏輯給處理一下先。爲了不影響UI線程(以後可能數據處理會有點多),我就使用了HandlerThread這個類來進行異步操作,先把類初始化
mHandlerThread = new HandlerThread("liveHandlerThread");
mHandlerThread.start();
mHandler = new LiveHandler(this, mHandlerThread.getLooper());
LiveHandler是我定義在LiveActivity的靜態內部類,用來進行異步操作的
private static class LiveHandler extends Handler {
private WeakReference<LiveActivity> mActivity;
public LiveHandler(LiveActivity activity, Looper looper) {
super(looper);
mActivity = new WeakReference<LiveActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
LiveActivity activity = mActivity.get();
if(activity == null) {
return;
}
switch (msg.what) {
case STREAMER_INIT:
break;
case STREAMER_HANDLE:
Bundle bundle = msg.getData();
if(bundle != null) {
byte[] data = bundle.getByteArray("frame_data");
if(data != null && data.length > 0) {
activity.streamerHandle(data);
} else {
Log.e("LiveActivity", "byte data null");
}
} else {
Log.e("LiveActivity", "bundle null");
}
break;
case STREAMER_FLUSH:
activity.streamerFlush();
break;
case STREAMER_RELEASE:
activity.streamerRelease();
break;
}
}
}
LiveActivity裏面的邏輯主要是一些細節的處理,完整的代碼就下面那樣:
package com.xiaoxiao.live;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.lang.ref.WeakReference;
/**
* Created by Administrator on 2017/2/20.
*/
public class LiveActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
private static final int STREAMER_INIT = 0;
private static final int STREAMER_HANDLE = 1;
private static final int STREAMER_RELEASE = 2;
private static final int STREAMER_FLUSH = 3;
private Camera mCamera;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private int mCameraId = 0;
private int width = 720;
private int height = 480;
/**
* 判斷有沒有初始化成功,不成功不不進行後續的編碼處理
*/
private int liveInitResult = -1;
/**
* 異步操作
*/
private HandlerThread mHandlerThread;
private LiveHandler mHandler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live);
mSurfaceView = (SurfaceView) findViewById(R.id.live_sv_live);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.setFixedSize(width, height);
mSurfaceHolder.addCallback(this);
mHandlerThread = new HandlerThread("liveHandlerThread");
mHandlerThread.start();
mHandler = new LiveHandler(this, mHandlerThread.getLooper());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_live, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.checkable_menu) {
boolean isChecked = item.isChecked();
Log.e("LiveActivity", "checked: " + isChecked);
item.setChecked(!isChecked);
mCameraId = 1 - mCameraId;
destroyCamera();
initCamera();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
/**
* 如果初始化成功,那就把數據發送到Handler,然後再調用native方法
*/
if(liveInitResult == 0 && data != null && data.length > 0) {
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putByteArray("frame_data", data);
msg.what = STREAMER_HANDLE;
msg.setData(bundle);
mHandler.sendMessage(msg);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
/**
* 在surface創建的時候進行初始化,如果失敗了,也是需要釋放已經開闢了的資源
*/
liveInitResult = streamerInit(width, height);
if(liveInitResult == -1) {
mHandler.sendEmptyMessage(STREAMER_RELEASE);
} else {
Log.e("LiveActivity", "streamer init result: " + liveInitResult);
}
initCamera();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
/**
* 在surface銷燬的時候清空緩衝幀(在直播成功開啓的情況下)
* 清空後就進行資源的釋放
* 並且把HandlerThread退出
*/
if(liveInitResult == 0) {
mHandler.sendEmptyMessage(STREAMER_FLUSH);
}
mHandler.sendEmptyMessage(STREAMER_RELEASE);
mHandlerThread.quitSafely();
destroyCamera();
}
private void initCamera() {
try {
mCamera = Camera.open(mCameraId);
mCamera.setPreviewDisplay(mSurfaceHolder);
Camera.Parameters params = mCamera.getParameters();
//設置預覽大小
params.setPreviewSize(width, height);
//設置生成的照片大小
params.setPictureSize(width, height);
params.setPreviewFormat(ImageFormat.NV21);
mCamera.setDisplayOrientation(90);
//params.setRotation(90);
/*List<Camera.Size> sizes = params.getSupportedPreviewSizes();
for(Camera.Size s : sizes) {
Log.e("LiveActivity", s.width + " X " + s.height);
}*/
mCamera.setParameters(params);
mCamera.setPreviewCallback(this);
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
private void destroyCamera() {
if(mCamera == null) {
return;
}
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
/**
* 初始化編碼的一些東西,比如編碼器等
* @param width 編碼視頻的寬
* @param height 編碼視頻的高
* @return 0 成功 小於0失敗
*/
private native int streamerInit(int width, int height);
/**
* 對每一次預覽的數據進行編碼推流
* @param data NV21格式的數據
* @return 0成功,小於0失敗
*/
private native int streamerHandle(byte[] data);
/**
* 把緩衝幀的數據清空
* @return 0成功,小於0失敗
*/
private native int streamerFlush();
/**
* 釋放資源,比如編碼器這些
* @return 0成功,小於0失敗
*/
private native int streamerRelease();
//------------------------------------------------------------------------
private static class LiveHandler extends Handler {
private WeakReference<LiveActivity> mActivity;
public LiveHandler(LiveActivity activity, Looper looper) {
super(looper);
mActivity = new WeakReference<LiveActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
LiveActivity activity = mActivity.get();
if(activity == null) {
return;
}
switch (msg.what) {
case STREAMER_INIT:
break;
case STREAMER_HANDLE:
Bundle bundle = msg.getData();
if(bundle != null) {
byte[] data = bundle.getByteArray("frame_data");
if(data != null && data.length > 0) {
activity.streamerHandle(data);
} else {
Log.e("LiveActivity", "byte data null");
}
} else {
Log.e("LiveActivity", "bundle null");
}
break;
case STREAMER_FLUSH:
activity.streamerFlush();
break;
case STREAMER_RELEASE:
activity.streamerRelease();
break;
}
}
}
}
那麼,寫完LiveActivity的邏輯後,就要進入重要的內容了,就是在c裏面完成編碼以及推流的操作
//
// Created by Administrator on 2017/2/19.
//
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/time.h"
#include "libavutil/imgutils.h"
#define LOG_TAG "FFmpeg"
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, format, ##__VA_ARGS__)
#define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, format, ##__VA_ARGS__)
AVFormatContext *ofmt_ctx = NULL;
AVStream *out_stream = NULL;
AVPacket pkt;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *yuv_frame;
int frame_count;
int src_width;
int src_height;
int y_length;
int uv_length;
int64_t start_time;
/**
* 回調函數,用來把FFmpeg的log寫到sdcard裏面
*/
void live_log(void *ptr, int level, const char* fmt, va_list vl) {
FILE *fp = fopen("/sdcard/123/live_log.txt", "a+");
if(fp) {
vfprintf(fp, fmt, vl);
fflush(fp);
fclose(fp);
}
}
/**
* 編碼函數
* avcodec_encode_video2被deprecated後,自己封裝的
*/
int encode(AVCodecContext *pCodecCtx, AVPacket* pPkt, AVFrame *pFrame, int *got_packet) {
int ret;
*got_packet = 0;
ret = avcodec_send_frame(pCodecCtx, pFrame);
if(ret <0 && ret != AVERROR_EOF) {
return ret;
}
ret = avcodec_receive_packet(pCodecCtx, pPkt);
if(ret < 0 && ret != AVERROR(EAGAIN)) {
return ret;
}
if(ret >= 0) {
*got_packet = 1;
}
return 0;
}
JNIEXPORT jstring JNICALL
Java_com_xiaoxiao_live_MainActivity_helloFromFFmpeg(JNIEnv *env, jobject instance) {
// TODO
char info[10000] = {0};
sprintf(info, "%s\n", avcodec_configuration());
return (*env)->NewStringUTF(env, info);
}
JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerRelease(JNIEnv *env, jobject instance) {
// TODO
if(pCodecCtx) {
avcodec_close(pCodecCtx);
pCodecCtx = NULL;
}
if(ofmt_ctx) {
avio_close(ofmt_ctx->pb);
}
if(ofmt_ctx) {
avformat_free_context(ofmt_ctx);
ofmt_ctx = NULL;
}
if(yuv_frame) {
av_frame_free(&yuv_frame);
yuv_frame = NULL;
}
}
JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerFlush(JNIEnv *env, jobject instance) {
// TODO
int ret;
int got_packet;
AVPacket packet;
if(!(pCodec->capabilities & CODEC_CAP_DELAY)) {
return 0;
}
while(1) {
packet.data = NULL;
packet.size = 0;
av_init_packet(&packet);
ret = encode(pCodecCtx, &packet, NULL, &got_packet);
if(ret < 0) {
break;
}
if(!got_packet) {
ret = 0;
break;
}
LOGI("Encode 1 frame size:%d\n", packet.size);
AVRational time_base = ofmt_ctx->streams[0]->time_base;
AVRational r_frame_rate1 = {60, 2};
AVRational time_base_q = {1, AV_TIME_BASE};
int64_t calc_duration = (double)(AV_TIME_BASE) * (1 / av_q2d(r_frame_rate1));
packet.pts = av_rescale_q(frame_count * calc_duration, time_base_q, time_base);
packet.dts = packet.pts;
packet.duration = av_rescale_q(calc_duration, time_base_q, time_base);
packet.pos = -1;
frame_count++;
ofmt_ctx->duration = packet.duration * frame_count;
ret = av_interleaved_write_frame(ofmt_ctx, &packet);
if(ret < 0) {
break;
}
}
//寫文件尾
av_write_trailer(ofmt_ctx);
return 0;
}
JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerHandle(JNIEnv *env, jobject instance,
jbyteArray data_) {
jbyte *data = (*env)->GetByteArrayElements(env, data_, NULL);
// TODO
int ret, i, resultCode;
int got_packet = 0;
resultCode = 0;
/**
* 這裏就是之前說的NV21轉爲AV_PIX_FMT_YUV420P這種格式的操作了
*/
memcpy(yuv_frame->data[0], data, y_length);
for (i = 0; i < uv_length; i++) {
*(yuv_frame->data[2] + i) = *(data + y_length + i * 2);
*(yuv_frame->data[1] + i) = *(data + y_length + i * 2 + 1);
}
yuv_frame->format = pCodecCtx->pix_fmt;
yuv_frame->width = src_width;
yuv_frame->height = src_height;
//yuv_frame->pts = frame_count;
yuv_frame->pts = (1.0 / 30) * 90 * frame_count;
pkt.data = NULL;
pkt.size = 0;
av_init_packet(&pkt);
//進行編碼
ret = encode(pCodecCtx, &pkt, yuv_frame, &got_packet);
if(ret < 0) {
resultCode = -1;
LOGE("Encode error\n");
goto end;
}
if(got_packet) {
LOGI("Encode frame: %d\tsize:%d\n", frame_count, pkt.size);
frame_count++;
pkt.stream_index = out_stream->index;
//寫PTS/DTS
AVRational time_base1 = ofmt_ctx->streams[0]->time_base;
AVRational r_frame_rate1 = {60, 2};
AVRational time_base_q = {1, AV_TIME_BASE};
int64_t calc_duration = (double)(AV_TIME_BASE) * (1 / av_q2d(r_frame_rate1));
pkt.pts = av_rescale_q(frame_count * calc_duration, time_base_q, time_base1);
pkt.dts = pkt.pts;
pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base1);
pkt.pos = -1;
//處理延遲
int64_t pts_time = av_rescale_q(pkt.dts, time_base1, time_base_q);
int64_t now_time = av_gettime() - start_time;
if(pts_time > now_time) {
av_usleep(pts_time - now_time);
}
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if(ret < 0) {
LOGE("Error muxing packet");
resultCode = -1;
goto end;
}
av_packet_unref(&pkt);
}
end:
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
return resultCode;
}
JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerInit(JNIEnv *env, jobject instance, jint width,
jint height) {
// TODO
int ret = 0;
const char *address = "rtmp://192.168.1.102/oflaDemo/test";
src_width = width;
src_height = height;
//yuv數據格式裏面的 y的大小(佔用的空間)
y_length = width * height;
//u/v佔用的空間大小
uv_length = y_length / 4;
//設置回調函數,寫log
av_log_set_callback(live_log);
//激活所有的功能
av_register_all();
//推流就需要初始化網絡協議
avformat_network_init();
//初始化AVFormatContext
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", address);
if(!ofmt_ctx) {
LOGE("Could not create output context\n");
return -1;
}
//尋找編碼器,這裏用的就是x264的那個編碼器了
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if(!pCodec) {
LOGE("Can not find encoder!\n");
return -1;
}
//初始化編碼器的context
pCodecCtx = avcodec_alloc_context3(pCodec);
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; //指定編碼格式
pCodecCtx->width = width;
pCodecCtx->height = height;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 30;
pCodecCtx->bit_rate = 800000;
pCodecCtx->gop_size = 300;
if(ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
pCodecCtx->max_b_frames = 3;
AVDictionary *dicParams = NULL;
av_dict_set(&dicParams, "preset", "ultrafast", 0);
av_dict_set(&dicParams, "tune", "zerolatency", 0);
//打開編碼器
if(avcodec_open2(pCodecCtx, pCodec, &dicParams) < 0) {
LOGE("Failed to open encoder!\n");
return -1;
}
//新建輸出流
out_stream = avformat_new_stream(ofmt_ctx, pCodec);
if(!out_stream) {
LOGE("Failed allocation output stream\n");
return -1;
}
out_stream->time_base.num = 1;
out_stream->time_base.den = 30;
//複製一份編碼器的配置給輸出流
avcodec_parameters_from_context(out_stream->codecpar, pCodecCtx);
//打開輸出流
ret = avio_open(&ofmt_ctx->pb, address, AVIO_FLAG_WRITE);
if(ret < 0) {
LOGE("Could not open output URL %s", address);
return -1;
}
ret = avformat_write_header(ofmt_ctx, NULL);
if(ret < 0) {
LOGE("Error occurred when open output URL\n");
return -1;
}
//初始化一個幀的數據結構,用於編碼用
//指定AV_PIX_FMT_YUV420P這種格式的
yuv_frame = av_frame_alloc();
uint8_t *out_buffer = (uint8_t *) av_malloc(av_image_get_buffer_size(pCodecCtx->pix_fmt, src_width, src_height, 1));
av_image_fill_arrays(yuv_frame->data, yuv_frame->linesize, out_buffer, pCodecCtx->pix_fmt, src_width, src_height, 1);
start_time = av_gettime();
return 0;
}
這裏面有一個值得注意的就是NV21的數據處理那裏
/**
* 這裏就是之前說的NV21轉爲AV_PIX_FMT_YUV420P這種格式的操作了
*/
memcpy(yuv_frame->data[0], data, y_length);
for (i = 0; i < uv_length; i++) {
*(yuv_frame->data[2] + i) = *(data + y_length + i * 2);
*(yuv_frame->data[1] + i) = *(data + y_length + i * 2 + 1);
}
可以對照着前面說的yuv的數據存儲來看,這樣子就會明白爲什麼要這樣處理一下了,明白了這個,那YV12的處理也很容易了
那麼寫完這個c代碼後,我們就可以把服務器給配置一下了,這樣子就可以調試我們的直播代碼有沒有問題了
上一篇文章裏面說了,直播需要一個流媒體服務器,現在可以用nginx 然後裝個RTMP的模塊就可以了(戰鬥民族寫的),還有其他的就是FMS,red5.
我這裏使用的就是red5,java寫的,開源的。我們把它下載下來,然後解壓就行了
運行起來後,就可以在瀏覽器裏面輸入http://localhost:5080/ 如果能打開red5的頁面就說明已經運行起來了
打開demos
如果ofla這個demo存在的話,打開就可以看到下面的頁面了
在這裏面有兩個直接協議的實現了,一個是RTMP,一個是RTMPT(是RTMP的變種,相當於RTMP用http包裝後的協議)。
點擊那個播放的圖標就可以播放流媒體了,但是要直播我們app的流還需要配置一點東西,在red5的根目錄下打開webapps/oflaDemo這個目錄
用編輯器打開index.html,把rtmp那個播放器的腳本修改成下面的
<center>
<b>RTMP</b>
<div id='mediaspace'>This text will be replaced</div>
<script type='text/javascript'>
jwplayer('mediaspace').setup({
'flashplayer': 'player.swf',
'file': 'test',
'streamer': 'rtmp://192.168.1.102/oflaDemo',
'controlbar': 'bottom',
'width': '720',
'height': '480'
});
</script>
<br />
<b>RTMPT</b>
<div id='mediaspace2'>This text will be replaced</div>
<script type='text/javascript'>
jwplayer('mediaspace2').setup({
'flashplayer': 'player.swf',
'file': 'BladeRunner2049.flv',
'streamer': 'rtmpt://localhost:5080/oflaDemo',
'controlbar': 'bottom',
'width': '720',
'height': '480'
});
</script>
</center>
rtmp://192.168.1.102/oflaDemo這個地址和我們在c裏面寫的那個address是不是一樣,然後我們再指定了它的file是test
完整的就是我們在c裏面寫的那個address了const char *address = "rtmp://192.168.1.102/oflaDemo/test";
,所以這個配置一定要正確,不然就無法直播了
192.168.1.102是我電腦的ip,完成這個調試要求手機和電腦在同一局域網下,除非自己有外網的流媒體服務器就另說了
手機那個地址千萬不要寫localhost,都不是同一個機器
好了,配置完這個之後,我們再重新刷新一下我們的網頁。然後就可以調試我們的直播了
點擊我要直播,然後就可以點擊網頁的那個播放圖標了,這樣子就可以調試我們的直播了。由於手機電腦互調,弄不了圖片,所以就要各位自己運行看結果了
總結
那麼到這裏,我們就已經完成了camera的處理,以及推流成功了,通過red5服務器,也可以看到了我們的直播,但現在這個直播還有幾個問題要處理先的:
- 看到的直播和手機上的有一個旋轉的差別(這個原因是因爲手機攝像頭的預覽我們設置了旋轉,以方便豎屏直播,但是這個設置是不會影響原始數據的旋轉的,而且沒法設置,所以就會產生這個bug)
- 有延遲,這個應該是PTS/DTS的問題
- 沒有聲音
上面那幾個問題都是需要處理好的,那麼下一篇我們就會先把前面的兩個問題給處理一下