最纯粹的直播技术实战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的点击操作了

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTQ4NTUzMQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="click" title="">

我们把上一次的測试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里面配置一下

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTQ4NTUzMQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="manifest" title="">

假设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写的,开源的。我们把它下载下来,然后解压即可了

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTQ4NTUzMQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="red5" title="">

执行起来后,就能够在浏览器里面输入http://localhost:5080/ 假设能打开red5的页面就说明已经执行起来了

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTQ4NTUzMQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="red5" title="">

打开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的处理以及推流的更多相关文章

  1. 最纯粹的直播技术实战03-通过filter进行旋转及卡顿修复

    最纯粹的直播技术实战03-通过filter进行旋转及卡顿修复 最新实战教程,Android自己主动化刷量.作弊与防作弊,案例:刷友盟统计.批量注冊苹果帐号 这个系列的文章将会研究最纯粹的Android ...

  2. 技术实战:基于 MHA 方式实现 MySQL 的高可用(转)

    转自:http://os.51cto.com/art/201307/401702_all.htm MHA故障转移可以很好的帮我们解决从库数据的一致性问题,同时最大化挽回故障发生后的数据.本文分享了基于 ...

  3. 「视频直播技术详解」系列之七:直播云 SDK 性能测试模型

    ​关于直播的技术文章不少,成体系的不多.我们将用七篇文章,更系统化地介绍当下大热的视频直播各环节的关键技术,帮助视频直播创业者们更全面.深入地了解视频直播技术,更好地技术选型. 本系列文章大纲如下: ...

  4. 手游录屏直播技术详解 | 直播 SDK 性能优化实践

    在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...

  5. Apache Spark技术实战之4 -- 利用Spark将json文件导入Cassandra

    欢迎转载,转载请注明出处. 概要 本文简要介绍如何使用spark-cassandra-connector将json文件导入到cassandra数据库,这是一个使用spark的综合性示例. 前提条件 假 ...

  6. 直播技术资源站 http://lib.csdn.net/base/liveplay/structure

    直播技术资源站    http://lib.csdn.net/base/liveplay/structure

  7. 实现简易的android 直播技术

    Android 的直播,主要使用的是camera采集的数据推流到服务器上,在客户端播放camera采集的数据.采用SurfaceView+ SurfaceTexture来显示camera数据, Sur ...

  8. HTTP Live Streaming直播(iOS直播)技术分析与实现

    本文转载自:http://www.cnblogs.com/haibindev/archive/2013/01/30/2880764.html 不经意间发现,大半年没写博客了,自觉汗颜.实则2012后半 ...

  9. 爬虫技术实战 | WooYun知识库

    爬虫技术实战 | WooYun知识库 爬虫技术实战 大数据分析与机器学习领域Python兵器谱-大数据邦-微头条(wtoutiao.com) 大数据分析与机器学习领域Python兵器谱

随机推荐

  1. Linux下解压tar.xz文件

    xz -d glib-2.14.tar.xz tar -xvf glib-2.14.tar 前面一个是将xz文件解压成tar文件,后面一个是将tar文件解压. xz使用格式:压缩: xz -z fil ...

  2. RocketMQ 拉取消息-通信模块

    首先看server端:class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer 下面这个实现了 ...

  3. 创建CSS3警示框渐变动画

    来源:GBin1.com 在线演示   在线下载 现代的网页设计技术已经允许开发人员在大多数浏览器中快速实现所支持的动画,其中消息警示是非常普遍的.由于默认的JavaScript警示框往往设计不佳和过 ...

  4. 流媒体播放mime类型添加

    .m3u8 application/x-mpegURL.ts video/MP2T

  5. UIView的transform属性

    一.什么是Transform Transform(变化矩阵)是一种3×3的矩阵,如下图所示: 通过这个矩阵我们可以对一个坐标系统进行缩放,平移,旋转以及这两者的任意组着操作.而且矩阵的操作不具备交换律 ...

  6. mysql创建用户、授权,revoke

     use mysql;set password for root =password('haowumz');select host,user,password from user ;show gran ...

  7. php使用curl请求数据(采集数据)

    <?php $url = "http://www.baidu.com/s?wd=刘俊涛的博客"; $header = array( 'User-Agent: Mozilla/ ...

  8. Android Exception 8(Couldn't read row 0, col -1 from CursorWindow)

    java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow.  Make sure the Curso ...

  9. 在DATASET中要是想添加进另一个表怎么办?

    问:sql="select * from banzhu_manage ";adapter=new SqlDataAdapter(sql,banzhu_conn);adapter.F ...

  10. css hacks

    /***** Selector Hacks ******/ /* IE6 and below */ * html #uno { color: red } /* IE7 */ *:first-child ...