一、整体思路
1.摄像头实时捕获视频并进行h264编码
2.接收wifi的视频数据并实时在另一个监控视频手机查看,接收到的数据进行h264解码
3.wifi的数据传输利用socket通信
二、摄像头捕获数据并进行h264编码
摄像头捕获数据
打开相机,设置预览画面,设置监听获取视频流的每一帧:
/**
* 打开相机
*/
private void openCamera() {
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
//获取相机参数
Camera.Parameters parameters = camera.getParameters();
//获取相机支持的预览的大小
Camera.Size previewSize = getCameraPreviewSize(parameters);
int width = previewSize.width;
int height = previewSize.height;
Log.d("AAA",""+width+" "+height);
//设置预览格式(也就是每一帧的视频格式)YUV420下的NV21
parameters.setPreviewFormat(ImageFormat.NV21);
//设置预览图像分辨率
parameters.setPreviewSize(width, height);
//相机旋转90度
camera.setDisplayOrientation(90);
//配置camera参数
camera.setParameters(parameters);
try {
camera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
}
//设置监听获取视频流的每一帧
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}
});
nv21EncoderH264 = new NV21EncoderH264(width, height);
nv21EncoderH264.setEncoderListener(this);
//调用startPreview()用以更新preview的surface
camera.startPreview();
camera.autoFocus(null);
}
//设置监听获取视频流的每一帧
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
nv21EncoderH264.encoderH264(data);
}
});
对数据进行h264编码:
public class NV21EncoderH264 {
private int width, height;
private int frameRate = 30;
private MediaCodec mediaCodec;
private EncoderListener encoderListener;
public NV21EncoderH264(int width, int height) {
this.width = width;
this.height = height;
initMediaCodec();
}
private void initMediaCodec() {
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
//height和width一般都是照相机的height和width。
//TODO 因为获取到的视频帧数据是逆时针旋转了90度的,所以这里宽高需要对调
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
//描述平均位速率(以位/秒为单位)的键。 关联的值是一个整数
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//描述视频格式的帧速率(以帧/秒为单位)的键。帧率,一般在15至30之内,太小容易造成视频卡顿。
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
//色彩格式,具体查看相关API,不同设备支持的色彩格式不尽相同
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//关键帧间隔时间,单位是秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//开始编码
mediaCodec.start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将NV21编码成H264
*/
public void encoderH264(byte[] data) {
//将NV21编码成NV12
byte[] bytes = NV21ToNV12(data, width, height);
//视频顺时针旋转90度
byte[] nv12 = rotateNV290(bytes, width, height);
try {
//拿到输入缓冲区,用于传送数据进行编码
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
//拿到输出缓冲区,用于取到编码后的数据
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
//当输入缓冲区有效时,就是>=0
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//往输入缓冲区写入数据
inputBuffer.put(nv12);
//五个参数,第一个是输入缓冲区的索引,第二个数据是输入缓冲区起始索引,第三个是放入的数据大小,第四个是时间戳,保证递增就是
mediaCodec.queueInputBuffer(inputBufferIndex, 0, nv12.length, System.nanoTime() / 1000, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//拿到输出缓冲区的索引
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
//outData就是输出的h264数据
if (encoderListener != null) {
encoderListener.h264(outData);
}
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
/**
* 因为从MediaCodec不支持NV21的数据编码,所以需要先讲NV21的数据转码为NV12
*/
private byte[] NV21ToNV12(byte[] nv21, int width, int height) {
byte[] nv12 = new byte[width * height * 3 / 2];
int frameSize = width * height;
int i, j;
System.arraycopy(nv21, 0, nv12, 0, frameSize);
for (i = 0; i < frameSize; i++) {
nv12[i] = nv21[i];
}
for (j = 0; j < frameSize / 2; j += 2) {
nv12[frameSize + j - 1] = nv21[j + frameSize];
}
for (j = 0; j < frameSize / 2; j += 2) {
nv12[frameSize + j] = nv21[j + frameSize - 1];
}
return nv12;
}
/**
* 此处为顺时针旋转旋转90度
*
* @param data 旋转前的数据
* @param imageWidth 旋转前数据的宽
* @param imageHeight 旋转前数据的高
* @return 旋转后的数据
*/
private byte[] rotateNV290(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
i = imageWidth * imageHeight * 3 / 2 - 1;
for (int x = imageWidth - 1; x > 0; x = x - 2) {
for (int y = 0; y < imageHeight / 2; y++) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i--;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
i--;
}
}
return yuv;
}
/**
* 设置编码成功后数据回调
*/
public void setEncoderListener(EncoderListener listener) {
encoderListener = listener;
}
public interface EncoderListener {
void h264(byte[] data);
}
}
三、接收到的数据进行h264解码
package com.example.receiver;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class H264DeCodePlay {
private static final String TAG = "zqf-dev";
//使用android MediaCodec解码
private MediaCodec mediaCodec;
private Surface surface;
MediaCodec.BufferInfo mediaCodecBufferInfo;
H264DeCodePlay(Surface surface) {
this.surface = surface;
initMediaCodec();
}
private void initMediaCodec() {
try {
//创建解码器 H264的Type为 AAC
mediaCodec = MediaCodec.createDecoderByType("video/avc");
//创建配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 1080, 1920);
//设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
//配置绑定mediaFormat和surface
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodecBufferInfo = new MediaCodec.BufferInfo();
mediaCodec.start();
Log.e(TAG, "创建解码成功");
} catch (IOException e) {
e.printStackTrace();
//创建解码失败
Log.e(TAG, "创建解码失败");
}
}
/**
* 解码播放
*/
void decodePlay() {
}
public void decode(byte[] buf) {
try {
//获取MediaCodec的输入流
// ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
//设置解码等待时间,0为不等待,-1为一直等待,其余为时间单位
int inputBufferIndex = mediaCodec.dequeueInputBuffer(0);
//填充数据到输入流
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
} else {
//兼容安卓5.0以下,如不需要可以删掉
inputBuffer = mediaCodec.getInputBuffers()[inputBufferIndex];
}
// inputBuffer.put(buf);
//mediaCodec.queueInputBuffer(inputBufferIndex, 0,buf.length , System.nanoTime(), 0);
inputBuffer.put(buf, 0, buf.length);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, buf.length, System.nanoTime() , 0);
}
//解码数据到surface,实际项目中最好将以下代码放入另一个线程,不断循环解码以降低延迟
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(mediaCodecBufferInfo, 0);
if (outputBufferIndex >= 0) {
mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//此处可以或得到视频的实际分辨率,用以修正宽高比
//fixHW();
}
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
//读取一帧数据
private int findByFrame(byte[] bytes, int start, int totalSize) {
for (int i = start; i < totalSize - 4; i++) {
//对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
return i;
}
}
return -1;
}
private byte[] getBytes(String videoPath) throws IOException {
InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
int len;
int size = 1024;
byte[] buf;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
buf = new byte[size];
while ((len = is.read(buf, 0, size)) != -1)
bos.write(buf, 0, len);
buf = bos.toByteArray();
return buf;
}
}
四、wifi的数据传输利用socket通信
- 由于H264是一种帧间编码,只记录每一帧之间的变化,后期解码的时候只要在上一帧基础上算出变化就可以了。并且我们监听完视频流后就开始对每一帧进行h264编码,如果这时socket还没连接好,接受放就无法收到第一帧数据,从而无法对h264的数据进行解码,造成视频数据丢失,所以我们应该在socket链接后再对视频流进行监听,再进行编码、发送数据。
- 发送端的socket的连接:
class SocketConnectThread extends Thread{
public void run(){
Log.e("info", "run: ============线程启动" );
try {
//等待客户端的连接,Accept会阻塞,直到建立连接,
//所以需要放在子线程中运行。
mSocket = mServerSocket.accept();
os = new DataOutputStream(mSocket.getOutputStream());
mSocket.setSendBufferSize(1000000);
//设置监听获取视频流的每一帧
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
nv21EncoderH264.encoderH264(data);
}
});
} catch (Exception e) {
e.printStackTrace();
return;
}
Log.e("info","connect success========================================");
}
}
- 编码成功的回调:
//编码成功的回调
@Override
public void h264(byte[] data) {
Log.e("TAG1", data.length+ "");
try {
send(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 先在子节流中写入一帧数据的字节数组大小,这样接受端就能提前创建好对应大小的字节数组:
public void send( byte[] data) throws InterruptedException {
Thread thread=new Thread() {
@Override
public void run() {
try {
// socket.getInputStream()
if(mSocket!=null&&isSending) {
os.writeInt(data.length);
os.flush();
os.write(data);
os.flush();
// writer.writeUTF(str); // 写一个UTF-8的信息
}
} catch (IOException e) {
socketConnectThread=new SocketConnectThread();
socketConnectThread.start();
e.printStackTrace();
}
}
};
executorService.submit(thread);
}
接收端接受数据并进行解码,在进行测试的时候,由于每次发送的字节数组过大,read()可能没办法每次都能读完,这里我们再加个循环保证接收到的数据是完整的:
new Thread(){
@Override
public void run() {
DataInputStream reader= new DataInputStream(mInStream);
try {
while (true) {
// 获取读取流
int length=reader.readInt();
byte[] bytes =new byte[length];
System.out.println("*等待客户端输入*");
int index = 0;
int len = 0;
while(index < length){
len = reader.read(bytes,index,length - index);
//每次读取完判断数据是否全部读取完毕
if(len > 0){
index += len;
}else {
break;
}
}
Log.e("TAG1", length+ " "+bytes.length);
h264DeCodePlay.decode(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
本文demo(发送端):https://github.com/gujunhe/client