這個系列的文章將會研究最純粹的Android直播的實現,並且不是用如今的集成SDK來達到直播的技術實現,而是從一個比較底層的直播實現來探討這個技術,這樣子對於直播技術的實現,現成的一些直播框架等都有一個比較好的理解。javascript
上一篇文章裏面,咱們完成了FFmpeg的編譯,而後也把編譯出來的庫運行在了Android上,那接下來就要處理Android的Camera以及推流的實現了。若是沒有看過上一篇文章的能夠戳這裏html
咱們會使用上一篇文章那工程項目來繼續後續的功能編寫,產生,咱們在MainActivity裏面添加兩個Button,一個是直播的,一個是看直播的java
<?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的點擊操做了android
咱們把上一次的測試TextView註釋掉了,而後新建了一個LiveActivity來處理Camera以及推流,主要就是展現直播。
後續還會有看直播的處理,就要就是拉流以及視頻的播放了
接下來就要在LiveActivity裏面處理一下Camera的東西了,首先要在LiveActivity的layout裏面添加一個SurfaceViewnginx
<?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的處理就會顯得比較的簡單了,web
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的問題
沒有聲音
上面那幾個問題都是須要處理好的,那麼下一篇咱們就會先把前面的兩個問題給處理一下