您的位置:首页
最纯粹的直播技术实战02-Camera的处理以及推流
2017-08-07 12:18
615 查看
最纯粹的直播技术实战02-Camera的处理以及推流
最新实战教程。Android自己主动化刷量、作弊与防作弊。案例:刷友盟统计、批量注冊苹果帐号
这个系列的文章将会研究最纯粹的Android直播的实现,并且不是用如今的集成SDK来达到直播的技术实现,而是从一个比較底层的直播实现来探讨这个技术,这样子对于直播技术的实现,现成的一些直播框架等都有一个比較好的理解。上一篇文章里面,我们完毕了FFmpeg的编译,然后也把编译出来的库执行在了Android上,那接下来就要处理Android的Camera以及推流的实现了。假设没有看过上一篇文章的能够戳这里
我们会使用上一篇文章那project项目来继续兴许的功能编写,产生,我们在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的处理也非常easy了
那么写完这个c代码后。我们就能够把server给配置一下了,这样子就能够调试我们的直播代码有没有问题了
上一篇文章里面说了。直播须要一个流媒体server。如今能够用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,完毕这个调试要求手机和电脑在同一局域网下。除非自己有外网的流媒体server就另说了
手机那个地址千万不要写localhost,都不是同一个机器
好了。配置完这个之后。我们再又一次刷新一下我们的网页。
然后就能够调试我们的直播了
点击我要直播,然后就能够点击网页的那个播放图标了,这样子就能够调试我们的直播了。由于手机电脑互调,弄不了图片,所以就要各位自己执行看结果了
总结
那么到这里,我们就已经完毕了camera的处理。以及推流成功了,通过red5server。也能够看到了我们的直播,但如今这个直播还有几个问题要处理先的:看到的直播和手机上的有一个旋转的区别(这个原因是由于手机摄像头的预览我们设置了旋转,以方便竖屏直播,可是这个设置是不会影响原始数据的旋转的。并且没法设置。所以就会产生这个bug)
有延迟,这个应该是PTS/DTS的问题
没有声音
上面那几个问题都是须要处理好的。那么下一篇我们就会先把前面的两个问题给处理一下
资源下载
相关文章推荐
- 最纯粹的直播技术实战02-Camera的处理以及推流
- 最纯粹的直播技术实战01-FFmpeg的编译与运行
- 最纯粹的直播技术实战03-通过filter进行旋转及卡顿修复
- 大数据处理技术 - 基于Hadoop的实战
- 航天科工“大数据处理技术—基于Hadoop的实战”圆满完成!
- 第02天实战技术(02):KVC字典转模型
- 技术分享之崩溃处理流程以及触发时机
- 第04天实战技术(02):block开发使用场景(代理传值)
- Spark Streaming实时流处理项目实战笔记02
- 实战hadoop海量数据处理系列02: hql执行工具
- 02Scala-函数定义、流程控制、异常处理入门实战
- 高手收集整理的baidu分词算法分析之一 查询处理以及分词技术(1)
- 大数据处理技术 - 基于Hadoop的实战
- Android多媒体技术(一)Camera实时视频采集预览、拍照、JPEG图片方向的处理
- 【SSH网上商城项目实战02】基本增删查改、Service和Action的抽取以及使用注解替换xml
- 「视频直播技术详解」系列之二:处理
- 实战hadoop海量数据处理系列02 番外篇: 在linux上使用hql执行工具 | hive排错记录
- 大数据处理技术 - 基于Hadoop的实战
- 数据获取以及处理系统 --- 技术规格说明书
- 第03天实战技术(06):网易新闻(标题居中处理)