这周给安排了个任务,在android端读取H264原始视频数据,传一帧播一帧,播就播吧,还要Socket实时传输实时播,虽然并没有接触过,都无从下手,不过想到直播都是这么个形式,说明肯定是行得通的,而且之前解码过音频,知道android有一个音视频很强大的类MediaCodec,搞了几天,终于解决了,决定写篇博客把这个感人的故事记录下来。
什么是H264,简单来说,h264是一种视频编码格式它是一种很高明的视频算法,视频播放的时候,差不多每秒会播24帧,如果以一部1280*720的2小时的电影为例,差不多会有148GB大小,显然这太庞大了,而采用h264编码,它会首先传一个关键帧(I 帧),然后比较下一帧跟上一帧之间的差异,将这两帧的差异作为一个补偿帧(P/B帧),这样的话,只需要传补偿帧就可以进行画面的更新,除非整个场景的切换,才会又传一个关键帧,不然的话只传一个补偿帧,数据量还是很小的,所以才把一部电影压缩到只有1-2GB这么大。
H264的数据结构。
头两帧是SPS(Sequence Parameter Set:序列参数集)和PPS(Picture Parameter Set:图像参数集)数据,这是视频的格式,包含了各种信息,我也没做深究,需要详细了解H264数据结构的朋友可以百度一下,有很多解释得很详细的,SPS,PPS后就是视频数据了,无论是I帧还是P帧,格式都是一样的,包括SPS和PPS,都是作为一帧数据,每一帧都是以0001或者001开头的,0001居多。所以我们只需要判断从0001开始,到下一个0001为止,就是一帧数据。大概需要了解的也就这么多,下面说说我是如何实现的。
建议大家一下,普通人用百度,程序员还是用Google的好,之前研究不会的东西的时候也是Google一下,顺利解决,百度上答案太多重复的了,所以虽然麻烦一点,不过能解决问题就好,还是推荐Google。
我是参考的这位大神:
http://www.itdadao.com/articles/c15a280703p0.html
不过他的帖子不太完全,贴一下我的代码:
首先是服务端:
一帧的封装类:
package com.example.server;
/**
* @brief 一帧的封装
*/
public class Frame {
public byte[] mData;
public int offset;
public int length;
public Frame(byte[] data, int offset, int size) {
mData = data;
this.offset = offset;
this.length = size;
}
public void setFrame(byte[] data, int offset, int size) {
mData = data;
this.offset = offset;
this.length = size;
}
}
VideoCodec接口:
package com.example.server;
public interface VideoCodec {
String MIME_TYPE = "video/avc";
int VIDEO_FRAME_PER_SECOND = 15;
int VIDEO_I_FRAME_INTERVAL = 5;
int VIDEO_BITRATE = 500 * 8 * 1000;
}
Socket服务器:
package com.example.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import android.util.Log;
public class Server {
private static Server server;
private static ServerSocket mServerSocket;
private Socket socket;
/* 服务器端口 */
private final static int SERVER_HOST_PORT = 12580;
private boolean release;
private static final String TAG = "daolema";
public boolean hasRelease() {
return release;
}
public void setRelease(boolean release) {
this.release = release;
}
public static Server getInstance() {
if (server == null) {
server = new Server();
}
return server;
}
private static ServerSocket getServerSocket() {
if (mServerSocket == null) {
try {
mServerSocket = new ServerSocket(SERVER_HOST_PORT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return mServerSocket;
}
public void connect() {
// TODO Auto-generated method stub
try {
mServerSocket = getServerSocket();
Log.i(TAG, "socket已开启,等待连接");
while (true) {
socket = mServerSocket.accept();
if (socket.isConnected()) {
Log.i(TAG, "socket连接成功");
break;
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void disconnect() {
// TODO Auto-generated method stub
try {
if (socket != null && socket.isConnected()) {
socket.close();
mServerSocket.close();
Log.i(TAG, "socket断开了");
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public byte[] readLength() {
try {
if (!socket.isConnected()) {
socket = mServerSocket.accept();
}
InputStream is = socket.getInputStream();
return readBytes(is, 4);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public byte[] readSPSPPS(int length) {
// TODO Auto-generated method stub
try {
if (!socket.isConnected()) {
socket = mServerSocket.accept();
}
InputStream is = socket.getInputStream();
return readBytes(is, length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public Frame readFrame(int frameLength) {
// TODO Auto-generated method stub
try {
if (!socket.isConnected()) {
socket = mServerSocket.accept();
}
InputStream is = socket.getInputStream();
Frame frame = new Frame(readBytes(is, frameLength), 0, frameLength);
return frame;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* 从socket读byte数组
*
* @param in
* @param length
* @return
*/
public static byte[] readBytes(InputStream in, long length) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
while (read < length) {
int cur = 0;
try {
cur = in.read(buffer, 0, (int) Math.min(1024, length - read));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (cur < 0) {
break;
}
read += cur;
baos.write(buffer, 0, cur);
}
return baos.toByteArray();
}
}
package com.example.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
public class VideoDecoder implements VideoCodec {
private Surface mSurface;
private static final int TYPE_SPS = 7;
private static final int TYPE_PPS = 8;
private static final int TYPE_FRAME_DATA = 5;
private static final int NO_FRAME_DATA = -1;
private final int TIMEOUT_US = 10000;
private static final String TAG = "daolema";
private Server mServer;
private Worker mWorker;
private byte[] mSps;
private byte[] mPps;
private final static int HEAD_OFFSET = 512;
public VideoDecoder(Surface surface, Server server)
// throws DecoderServerNullException
{
if (server == null) {
// throw new DecoderServerNullException();
}
mSurface = surface;
mServer = server;
}
public void start() {
if (mWorker == null) {
mWorker = new Worker();
mWorker.setRunning(true);
mWorker.start();
}
}
public void stop() {
if (mWorker != null) {
mWorker.setRunning(false);
mWorker = null;
}
if (mServer != null) {
if (!mServer.hasRelease()) {
mServer.disconnect();
}
}
}
private class Worker extends Thread {
volatile boolean isRunning;
private MediaCodec decoder;
private int mWidth;
private int mHeight;
MediaCodec.BufferInfo mBufferInfo;
/**
* 等待客户端连接,解码器配置
*
* @return
*/
public boolean prepare() {
mServer.connect();
mBufferInfo = new MediaCodec.BufferInfo();
// 首先读取编码的视频的长度和宽度
// try {
// mWidth = mServer.readInt();
// mHeight = mServer.readInt();
mWidth = 1280;
mHeight = 720;
// } catch (IOException e) {
// e.printStackTrace();
// return false;
// }
// 编码器那边会先发sps和pps来,头一帧就由sps和pps组成
int spsLength = bytesToInt(mServer.readLength());
byte[] sps = mServer.readSPSPPS(spsLength);
mSps = Arrays.copyOfRange(sps, 4, spsLength);
int ppsLength = bytesToInt(mServer.readLength());
byte[] pps = mServer.readSPSPPS(ppsLength);
mPps = Arrays.copyOfRange(pps, 4, ppsLength);
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, mHeight * mWidth);
format.setInteger(MediaFormat.KEY_MAX_HEIGHT, mHeight);
format.setInteger(MediaFormat.KEY_MAX_WIDTH, mWidth);
format.setByteBuffer("csd-0", ByteBuffer.wrap(mSps));
format.setByteBuffer("csd-1", ByteBuffer.wrap(mPps));
try {
decoder = MediaCodec
.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
} catch (IOException e) {
e.printStackTrace();
}
decoder.configure(format, mSurface, null, 0);
decoder.start();
return true;
}
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
if (!prepare()) {
Log.i(TAG, "视频解码器初始化失败");
isRunning = false;
}
while (isRunning) {
decode();
}
release();
}
private void decode() {
byte[] data = new byte[100000];
byte[] frameData = new byte[200000];
boolean isEOS = false;
while (!isEOS) {
// 判断是否是流的结尾
int inIndex = decoder.dequeueInputBuffer(TIMEOUT_US);
if (inIndex >= 0) {
/**
* 测试
*/
// byte[] frame=mServer.readFrame();
int frameLength = bytesToInt(mServer.readLength());
Frame frame = mServer.readFrame(frameLength);
ByteBuffer buffer = decoder.getInputBuffer(inIndex);
if (buffer == null) {
Log.i(TAG, "buffer=null");
return;
}
buffer.clear();
if (frame == null) {
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
decoder.queueInputBuffer(inIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEOS = true;
isRunning = false;
// 服务已经断开,释放服务端
mServer.disconnect();
} else {
buffer.put(frame.mData, 0, frame.length);
buffer.clear();
buffer.limit(frame.length);
decoder.queueInputBuffer(inIndex, 0, frame.length, 0,
MediaCodec.BUFFER_FLAG_SYNC_FRAME);
}
} else {
isEOS = true;
}
int outIndex = decoder.dequeueOutputBuffer(mBufferInfo,
TIMEOUT_US);
// Log.i(TAG, "video decoding .....");
while (outIndex >= 0) {
// ByteBuffer buffer =
decoder.getOutputBuffer(outIndex);
decoder.releaseOutputBuffer(outIndex, true);
outIndex = decoder.dequeueOutputBuffer(mBufferInfo,
TIMEOUT_US);// 再次获取数据,如果没有数据输出则outIndex=-1
// 循环结束
}
}
}
/**
* 释放资源
*/
@SuppressLint("NewApi")
private void release() {
if (decoder != null) {
decoder.stop();
decoder.release();
}
}
}
public int bytesToInt(byte[] bytes) {
int i;
i = (int) ((bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8)
| ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24));
return i;
}
}
在MainActivity中通过一个SurfaceView播放接收到的数据即可:
package com.example.server;
import android.app.Activity;
import android.os.Bundle;
import android.view.SurfaceView;
public class MainActivity extends Activity {
private SurfaceView mSurfaceView;
private Server mServer;
private VideoDecoder mVideoDecoder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView1);
mServer = Server.getInstance();
mVideoDecoder = new VideoDecoder(mSurfaceView.getHolder().getSurface(),
mServer);
mVideoDecoder.start();
}
}
服务器XML:
------------------------------------------------------------我是分隔线------------------------------------------------------------
再来是客户端,客户端就比较简单了:
Socket客户端:
package com.example.client;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import android.util.Log;
/**
* Socket通信的工具类(客户端)
* Created by 禽兽先生
* Created on 2016.03.06
*/
public class Client {
private static Client mClient;
private static Socket mSocket;
// 要连接的服务器IP地址
private static final String HOST_ADDRESS = "192.168.1.108";
// 要连接的服务器端口号
private static final int HOST_PORT = 12580;
/**
* 单例模式
*/
public static Client getInstance() {
if (mClient == null) {
mClient = new Client();
}
return mClient;
}
/**
* 打开Socket通道,连接服务器
*/
private static Socket getSocket() {
if (mSocket == null) {
try {
mSocket = new Socket(HOST_ADDRESS, HOST_PORT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return mSocket;
}
/**
* 打开Socket通道,连接服务器
*/
public void connect() {
// TODO Auto-generated method stub
mSocket = getSocket();
Log.i("daolema", "socket成功连接");
}
/**
* 断开Socket连接
*/
public void disconnect() {
if (mSocket != null && mSocket.isConnected()) {
try {
mSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void sendLength(byte[] bytes) {
try {
if (!mSocket.isConnected()) {
mSocket = getSocket();
}
OutputStream os = mSocket.getOutputStream();
os.write(bytes);
os.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void sendSPSPPS(byte[] bytes) {
try {
if (!mSocket.isConnected()) {
mSocket = getSocket();
}
OutputStream os = mSocket.getOutputStream();
os.write(bytes);
os.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void sendFrame(byte[] bytes) {
try {
if (!mSocket.isConnected()) {
mSocket = getSocket();
}
OutputStream os = mSocket.getOutputStream();
os.write(bytes, 0, bytes.length);
os.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端的MainActivity:
package com.example.client;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Arrays;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private Client mClient;
private byte[] fileBytes;
private int count = 0;
private Button bt_spspps;
private Button bt_h264;
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bt_spspps = (Button) findViewById(R.id.bt_spspps);
bt_h264 = (Button) findViewById(R.id.bt_h264);
fileBytes = getByte("/storage/emulated/0/Movies/1280_720.h264");
bt_spspps.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
new Thread(new Runnable() {
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@Override
public void run() {
mClient = Client.getInstance();
mClient.connect();
start: for (int i = 0; i < fileBytes.length; i++) {
if (fileBytes[i] == 0 && fileBytes[i + 1] == 0
&& fileBytes[i + 2] == 0
&& fileBytes[i + 3] == 1) {
end: for (int j = i + 4; j < fileBytes.length; j++) {
if (fileBytes[j] == 0
&& fileBytes[j + 1] == 0
&& fileBytes[j + 2] == 0
&& fileBytes[j + 3] == 1) {
byte[] temp = Arrays.copyOfRange(
fileBytes, i, j);
mClient.sendLength(intToBytes(temp.length));
mClient.sendSPSPPS(temp);
count++;
if (count == 2) {
runOnUiThread(new Runnable() {
public void run() {
bt_spspps.setEnabled(false);
}
});
return;
}
break end;
}
}
}
}
}
}).start();
}
});
bt_h264.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
new Thread(new Runnable() {
@SuppressWarnings("unused")
@Override
public void run() {
// TODO Auto-generated method stub
start: for (int i = 0; i < fileBytes.length; i++) {
if (fileBytes[i] == 0 && fileBytes[i + 1] == 0
&& fileBytes[i + 2] == 0
&& fileBytes[i + 3] == 1) {
end: for (int j = i + 4; j < fileBytes.length; j++) {
if (fileBytes[j] == 0
&& fileBytes[j + 1] == 0
&& fileBytes[j + 2] == 0
&& fileBytes[j + 3] == 1) {
byte[] temp = Arrays.copyOfRange(
fileBytes, i, j);
mClient.sendLength(intToBytes(temp.length));
mClient.sendFrame(temp);
break end;
}
}
}
}
}
}).start();
}
});
}
private byte[] getByte(String path) {
File f = new File(path);
InputStream in;
byte bytes[] = null;
try {
in = new FileInputStream(f);
bytes = new byte[(int) f.length()];
in.read(bytes);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return bytes;
}
public byte[] intToBytes(int i) {
byte[] bytes = new byte[4];
bytes[0] = (byte) (i & 0xff);
bytes[1] = (byte) ((i >> 8) & 0xff);
bytes[2] = (byte) ((i >> 16) & 0xff);
bytes[3] = (byte) ((i >> 24) & 0xff);
return bytes;
}
}
客户端XML:
这就是所有的代码了,记得在两边都加网络权限,不然没办法通信,读写内存卡的权限在客户端添加就行:
然后把h264文件放到对应目录下即可,你也可以自己随便放一个目录,代码中目录对应也行。
我写的这个是通过Socket实时传输数据的,实时播放的,如果需要直接播放本地的H264视频的朋友,可以参考这篇文章:
http://www.cnblogs.com/superping/p/4884689.html
本文的Demo:
http://download.csdn.net/detail/zgcqflqinhao/9688735