Android H265版视频通话项目
Posted nullZgy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android H265版视频通话项目相关的知识,希望对你有一定的参考价值。
H265视频压缩算法现在无疑是所有视频压缩技术中使用最广泛,最流行的。随着 x264/openH265以及ffmpeg等开源库的推出,大多数使用者无需再对H265的细节做过多的研究,这大降低了人们使用H265的成本。 H265重新利用了H264中定义的很多概念。两者都是基于块的视频编码技术,所以它们有着相同的根
源,和相近的编码方式,包括:
1、以宏块来细分图片,并最终以块来细分。
2、使用帧内压缩技术减少空间冗余。
3、使用帧内压缩技术减少时间冗余(运动估计和补偿)。
4、使用转换和量化来进行残留数据压缩。
5、使用熵编码减少残留和运动矢量传输和信号发送中的最后冗余。
事实上,视频编解码从MPEG-1诞生至今都没有根本性改进,H265也只是H264在一些关键性能上的更强进化以及简单化。
H265压缩技术主要采用了以下几种方法对视频数据进行压缩。包括:
帧内预测压缩,解决的是空域数据冗余问题。
帧间预测压缩(运动估计与补偿),解决的是时域数据冗徐问题。
整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
CABAC压缩。经过压缩后的帧分为:I帧,P帧和B帧:
I帧:关键帧,采用帧内压缩技术。 (完整编码的帧叫I帧)
P帧:向前参考帧,在压缩时,只参考前面已经处理的帧。采用帧音压缩技术。(参考之前的I帧生成的只包含差异部分编码的帧叫P帧)
B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。(参考前后的帧编码的帧叫B帧)
除了I/P/B帧外,还有图像序列GOP。
GOP:两个I帧之间是一个图像序列,在一个图像序列中只有一个I帧。详细的H265介绍参照简书的介绍 https://www.jianshu.com/p/85ef926e8a00.
先通过一个仿视频通话的列子介绍H265的使用。
先看效果图:
代码结构:一个Client 和一个Service
Client:
services
代码不多, 两个的代码99%相似,唯独webSocket连接上不一样。下面详细的介绍实现。
首先,一个SurfaceView显示对方的视频,自顶一个LocalSurfaceView显示自己的视频。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/call_btn"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#09ac44"
android:text="呼叫"/>
<SurfaceView
android:id="@+id/surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<zgy.services.LocalSurfaceView
android:id="@+id/local_surface"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_width="200dp"
android:layout_height="200dp" />
</RelativeLayout>
自定义一个LocalSurfeceView
package zgy.services;
import android.content.Context;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
public class LocalSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback {
private Camera.Size size;
private Camera mCamera;
EncodecPushLiveH265 encodecPushLiveH265;
public LocalSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
getHolder().addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
startPreview();
}
byte[] buffer;
private void startPreview() {
mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
Camera.Parameters parameters = mCamera.getParameters(); //流程
size = parameters.getPreviewSize(); //尺寸
try {
mCamera.setPreviewDisplay(getHolder());
mCamera.setDisplayOrientation(90); // 横着
buffer = new byte[size.width * size.height * 3 / 2];
mCamera.addCallbackBuffer(buffer);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { }
@Override
public void surfaceDestroyed(SurfaceHolder holder) { }
//接受到 MainActivity调用
public void startCaptrue(SocketLive.SocketCallback socketCallback){
encodecPushLiveH265 = new EncodecPushLiveH265(socketCallback, size.width, size.height);
encodecPushLiveH265.startLive();
}
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) { //获取到摄像头的原始数据yuv 开始 视频通话
if (encodecPushLiveH265 != null) {
encodecPushLiveH265.encodeFrame(bytes);
}
mCamera.addCallbackBuffer(bytes);
}
}
其中,解码用的核心。通过MediaCodec吧数据发给DSP芯片,再获取解码好的数dequeueOutputBuffer()
public int encodeFrame(byte[] input) { // Byte是摄像头采集的数据
// 旋转 nv21-nv12
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
nv12 = YuvUtils.nv21toNV12(input); //摄像头采集的数据是NV21,这里要先转成NV12,Android中MediaCodec只支持NV21数据
// 旋转
YuvUtils.portraitData2Raw(nv12, yuv, width, height);//将数据进行旋转
int inputBufferIndex = mediaCodec.dequeueInputBuffer(100000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(yuv);
long presentationTimeUs = computePresentationTime(frameIndex);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, yuv.length, presentationTimeUs, 0);
frameIndex++;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
dealFrame(outputBuffer, bufferInfo);
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
return 0;
}
通过Socket发送数据, 一般的第一帧是VPS 或 PPs,先缓存,和I帧P帧一起发送。
private void dealFrame(ByteBuffer bb, MediaCodec.BufferInfo bufferInfo) {
int offset = 4;
if (bb.get(2) == 0x01) { //
offset = 3;
}
int type = (bb.get(offset) & 0x7E) >> 1;
if (type == NAL_VPS) { // VPS 先缓存下,跟I帧一起发送
vps_sps_pps_buf = new byte[bufferInfo.size];
bb.get(vps_sps_pps_buf);
} else if (type == NAL_I) { //I 帧数据
final byte[] bytes = new byte[bufferInfo.size];
bb.get(bytes);
byte[] newBuf = new byte[vps_sps_pps_buf.length + bytes.length];
System.arraycopy(vps_sps_pps_buf, 0, newBuf, 0, vps_sps_pps_buf.length);
System.arraycopy(bytes, 0, newBuf, vps_sps_pps_buf.length, bytes.length);
this.socketLive.sendData(newBuf); //通过socket发送
} else { //P帧数据 发送
final byte[] bytes = new byte[bufferInfo.size];
bb.get(bytes);
this.socketLive.sendData(bytes);
// Log.v(TAG, "视频数据 " + Arrays.toString(bytes));
}
}
代码下载 https://download.csdn.net/download/u011694328/18778144
以上是关于Android H265版视频通话项目的主要内容,如果未能解决你的问题,请参考以下文章
Windows/Android/iOS平台视频播放器EasyPlayerEasyPlayer.js调用中无法播放H265视频流如何解决?