最近再弄实时视频通话方面,参考了很多资料,自己写了个demo,供大家参考,毕竟网上的资料也不多
先介绍视频的传输,通过H264编码,rtp协议进行传输,后面后github下载地址
一 流程:
1、通过Camera的回调函数,得到实时视频流
2、将得到的视频流通过H264编码
3、将H264编码后的数据打包成rtp包,并发送给对方
4、对方接受rtp包后,对视频流数据进行H264解码
5、将解码后的数据通过自定义view一帧帧的显示出来
二、H264编解码
得到的视频流数据太大,必须进行压缩后才能传输,这儿我才用了H264编码。
偷了个懒,demo中直接将别人的已经生成好的编解码库拿来用(.so文件),注意使用别人的.so文件,Java调用解码库中的类名,包名,方法名一定要与别人的一致,不然的话用不了。正常情况下直接需要通过NDK来生成。
三 RTP协议
这儿 我先简单的介绍一下rtp协议
RTP全名是Real-time Transport Protocol(实时传输协议),且配套的相关协议RTCP(Real-time Transport Control Protocol,即实时传输控制协议)。RTP用来为IP网上的语音、图像、传真等多种需要实时传输的多媒体数据提供端到端的实时传输服务。RTP为Internet上端到端的实时传输提供时间信息和流同步,但并不保证服务质量,服务质量由RTCP来提供。应用程序通常在 UDP 上运行 RTP 以便使用其多路结点和校验服务。
rtp协议中定义了时间戳,可以通过这个时间戳来同步音频流和视频流,这样就达到声音和视频的同步,实现视频通话。
有兴趣的盆友可以在网上查一下rtp协议的相关介绍和其包的结构
demo中用的rtp传输时参考spydroid这个开源的视频通话项目,网上也有对spydroid的分析。
核心代码:
rtp发送
//mCamera回调的类
class Callback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] frame, Camera camera) {
if (encoder_handle != -1) {
//底层函数,返回包的数目,返回包的大小存储在数组packetSize中,返回码流在stream中
send_packetNum = encode.EncoderOneFrame(encoder_handle, -1, frame, send_stream, send_packetSize);
Log.d("log", "原始数据大小:" + frame.length + " 转码后数据大小:" + send_stream.length);
if (send_packetNum > 0) {
//通过RTP协议发送帧
final int[] pos = {0}; //从码流头部开始取
final long timestamp = System.currentTimeMillis(); //设定时间戳
/**
* 因为可能传输数据过大 会将一次数据分割成好几段来传输
* 接受方 根据序列号和结束符 来将这些数据拼接成完整数据
*/
new Thread(new Runnable() {
@Override
public void run() {
int sequence = 0; //初始化序列号
for (int i = 0; i < send_packetNum; i++) {
rtp_send_packet.setPayloadType(2);//定义负载类型,视频为2
rtp_send_packet.setMarker(i == send_packetNum - 1 ? true : false); //是否是最后一个RTP包
rtp_send_packet.setSequenceNumber(sequence++); //序列号依次加1
rtp_send_packet.setTimestamp(timestamp); //时间戳
//Log.d("log", "序列号:" + sequence + " 时间:" + timestamp);
rtp_send_packet.setPayloadLength(send_packetSize[i]); //包的长度,packetSize[i]+头文件
//从码流stream的pos处开始复制,从socketBuffer的第12个字节开始粘贴,packetSize为粘贴的长度
System.arraycopy(send_stream, pos[0], socket_send_Buffer, 12, send_packetSize[i]); //把一个包存在socketBuffer中
pos[0] += send_packetSize[i]; //重定义下次开始复制的位置
//rtp_packet.setPayload(socketBuffer, rtp_packet.getLength());
// Log.d("log", "序列号:" + sequence + " bMark:" + rtp_packet.hasMarker() + " packetSize:" + packetSize[i] + " tPayloadType:2" + " timestamp:" + timestamp);
try {
rtp_socket.send(rtp_send_packet);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
}
rtp接收:
/**
* 接收rtp数据并解码 线程
*/
class DecoderThread extends Thread {
public void run() {
while (isRunning) {
try {
rtp_socket.receive(rtp_receive_packet); //接收一个包
} catch (IOException e) {
e.printStackTrace();
}
int packetSize = rtp_receive_packet.getPayloadLength(); //获取包的大小
if (packetSize <= 0)
continue;
if (rtp_receive_packet.getPayloadType() != 2) //确认负载类型为2
continue;
System.arraycopy(socket_receive_Buffer, 12, buffer, 0, packetSize); //socketBuffer->buffer
int sequence = rtp_receive_packet.getSequenceNumber(); //获取序列号
long timestamp = rtp_receive_packet.getTimestamp(); //获取时间戳
int bMark = rtp_receive_packet.hasMarker() == true ? 1 : 0; //是否是最后一个包
int frmSize = decode.PackH264Frame(decoder_handle, buffer, packetSize, bMark, (int) timestamp, sequence, frmbuf); //packer=拼帧器,frmbuf=帧缓存
Log.d("log", "序列号:" + sequence + " bMark:" + bMark + " packetSize:" + packetSize + " PayloadType:" + rtp_receive_packet.getPayloadType() + " timestamp:" + timestamp + " frmSize:" + frmSize);
if (frmSize <= 0)
continue;
decode.DecoderNal(frmbuf, frmSize, view.mPixel);//解码后的图像存在mPixel中
//Log.d("log","序列号:"+sequence+" 包大小:"+packetSize+" 时间:"+timestamp+" frmbuf[30]:"+frmbuf[30]);
view.postInvalidate();
}
//关闭
if (decoder_handle != 0) {
decode.DestroyH264Packer(decoder_handle);
decoder_handle = 0;
}
if (rtp_socket != null) {
rtp_socket.close();
rtp_socket = null;
}
decode.DestoryDecoder();
}
}
注意:
这儿将视频流通过H264压缩后,可能由于数据过大,这一次的数据要分成几个rtp包发送,send_packetNum就是分包的的个数,sequence为序列号,用来标记这是第几个包,setMarker是用来设置这是不是这次数据的最后一个包。接收端通过sequence,marker、时间戳 来对之次的数据进行重新组装,然后将组装后的数据进行解码显示。
四 原始视频流的获取
if (mCamera == null) {
//摄像头设置,预览视频
mCamera = Camera.open(1); //实例化摄像头类对象 0为后置 1为前置
Camera.Parameters p = mCamera.getParameters(); //将摄像头参数传入p中
p.setFlashMode("off");
p.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
p.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
p.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
//p.setPreviewFormat(PixelFormat.YCbCr_420_SP); //设置预览视频的格式
p.setPreviewFormat(ImageFormat.NV21);
p.setPreviewSize(352, 288); //设置预览视频的尺寸,CIF格式352×288
//p.setPreviewSize(800, 600);
p.setPreviewFrameRate(15); //设置预览的帧率,15帧/秒
mCamera.setParameters(p); //设置参数
byte[] rawBuf = new byte[1400];
mCamera.addCallbackBuffer(rawBuf);
mCamera.setDisplayOrientation(90); //视频旋转90度
try {
mCamera.setPreviewDisplay(holder); //预览的视频显示到指定窗口
} catch (IOException e) {
e.printStackTrace();
}
mCamera.startPreview(); //开始预览
//获取帧
//预览的回调函数在开始预览的时候以中断方式被调用,每秒调用15次,回调函数在预览的同时调出正在播放的帧
Callback a = new Callback();
mCamera.setPreviewCallback(a);
}
}
//mCamera回调的类
class Callback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] frame, Camera camera) {
}
在Callback 的onPreviewFrame函数中就能获得原始视频数据流,然后对这个数据流进行压缩,打包,发送
五 视频流的显示
/**
* 显示H264解码后视频 一帧一帧的按图片显示
*/
public class VideoPlayView extends View {
public int width = 352;
public int height = 288;
public byte[] mPixel = new byte[width * height * 2];
public ByteBuffer buffer = ByteBuffer.wrap(mPixel);
public Bitmap VideoBit = Bitmap.createBitmap(width, height, Config.RGB_565);
private Matrix matrix = null;
public Bitmap VideoBit2;
private RectF rectF;
public VideoPlayView(Context context, AttributeSet attrs) {
super(context, attrs);
matrix = new Matrix();
DisplayMetrics dm = getResources().getDisplayMetrics();
int W = dm.widthPixels;
int H = dm.heightPixels;
rectF = new RectF(0, 0, W, H);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
buffer.rewind();
VideoBit.copyPixelsFromBuffer(buffer);
setAngle();
//canvas.drawBitmap(adjustPhotoRotation(VideoBit,90), 0, 0, null);
//
//Bitmap b = BitmapFactory.decodeByteArray(mPixel, 0, mPixel.length);
canvas.drawBitmap(VideoBit2, null, rectF, null);
}
// 设置旋转比例
private void setAngle() {
matrix.reset();
matrix.setRotate(-90);
VideoBit2 = Bitmap.createBitmap(VideoBit, 0, 0, VideoBit.getWidth(),VideoBit.getHeight(), matrix, true);
}
private Bitmap adjustPhotoRotation(Bitmap bm, final int orientationDegree) {
Matrix m = new Matrix();
m.setRotate(orientationDegree, (float) bm.getWidth() / 2, (float) bm.getHeight() / 2);
float targetX, targetY;
if (orientationDegree == 90) {
targetX = bm.getHeight();
targetY = 0;
} else {
targetX = bm.getHeight();
targetY = bm.getWidth();
}
final float[] values = new float[9];
m.getValues(values);
float x1 = values[Matrix.MTRANS_X];
float y1 = values[Matrix.MTRANS_Y];
m.postTranslate(targetX - x1, targetY - y1);
Bitmap bm1 = Bitmap.createBitmap(bm.getHeight(), bm.getWidth(), Config.ARGB_8888);
Paint paint = new Paint();
Canvas canvas = new Canvas(bm1);
canvas.drawBitmap(bm, m, paint);
return bm1;
}
}
每次收到一帧的数据后,将解码后的数据放入mPixel 中,在刷新视图
decode.DecoderNal(frmbuf, frmSize, view.mPixel);//解码后的图像存在mPixel中
view.postInvalidate();
这儿只是实现点对点内网之间的视频实时传输,后面我应该还会继续将服务器、语音弄下去,到时再分享 :shock:
ps:我试过用Java udp服务器作为中转站,将两边的实时数据进行转发,效果也还行。但我还想实现外网中p2p之间的实时视频,用服务器作为中转站,将两边的实时ip和端口号分别转发给双方,这样两边就可以在外网进行点对点的视频,但我试过很多次,这样的udp打洞很难实现,因为服务器得到的客户端的端口号是这次连接的,但当进行p2p连接时,端口号可能又会发生变化。希望有大牛可以指点迷津。
guthub地址:[url]https://github.com/592713711/Android-VideoChat[/url]