概述
这一篇把摄像头的预览数据转化成H265码流发送给接收方,主要难点是
- 摄像头的数据是NV21,但是硬件编解码 MediaCodec 不支持NV21 ,所以要把 NV21转成 YUV420
- 摄像头厂商都是横着的,所以要选择90度
- 既然摄像头是横着的,所以YUV420的数据也要旋转90,(注意:既然数据也旋转了,那么MediaCodec的宽高就成了摄像头的高宽了)
MainActivity
主要是请求摄像头权限,开启 Socket,预览摄像头数据
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {
private static final int REQUEST_CODE_CAMERA = 100;
private SurfaceHolder mSurfaceHolder;
private PushSocket mPushSocket;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView surfaceview = findViewById(R.id.surfaceview);
surfaceview.getHolder().addCallback(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_CAMERA && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//申请成功,可以拍照
initSocket();
Toast.makeText(this, "有权限了", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "CAMERA PERMISSION DENIED", Toast.LENGTH_SHORT).show();
}
}
private void initSocket() {
mPushSocket = new PushSocket(this,mSurfaceHolder);
mPushSocket.start();
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
mSurfaceHolder = holder;
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
public void start(View view) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_CAMERA);
} else {
initSocket();
}
}
}
Socket
主要是把摄像头的数据转化成 H265发送出去
public class PushSocket implements Camera.PreviewCallback {
private static final String TAG = "PushSocket";
private WebSocket mWebSocket;
private Camera mCamera;
private Camera.Size mSize;
private SurfaceHolder mSurfaceHolder;
private byte[] mBuffer;
private byte[] nv12;
private MediaCodec mMediaCodec;
private byte[] mYuv420;
/**
* 端口号
*/
private static final int PORT = 13001;
private Context mContext;
public PushSocket(Context context, SurfaceHolder surfaceHolder) {
mContext = context;
mSurfaceHolder = surfaceHolder;
}
public void start() {
webSocketServer.start();
initCamera();
}
private WebSocketServer webSocketServer = new WebSocketServer(new InetSocketAddress(PORT)) {
@Override
public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) {
mWebSocket = webSocket;
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
Log.i(TAG, "onClose: 关闭 socket ");
}
@Override
public void onMessage(WebSocket webSocket, String message) {
}
@Override
public void onError(WebSocket conn, Exception e) {
Log.i(TAG, "onError: " + e.toString());
}
@Override
public void onStart() {
}
};
/**
* 发送数据
*
* @param bytes
*/
public void sendData(byte[] bytes) {
if (mWebSocket != null && mWebSocket.isOpen()) {
mWebSocket.send(bytes);
}
}
/**
* 关闭 Socket
*/
public void close() {
try {
mWebSocket.close();
webSocketServer.stop();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private void initCamera() {
mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
Camera.Parameters parameters = mCamera.getParameters();
mSize = parameters.getPreviewSize();
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
// 因为摄像头厂家都是横着的 所以把预览方向 调整
mCamera.setDisplayOrientation(90);
// 缓冲数据
mBuffer = new byte[mSize.width * mSize.height * 3 / 2];
mCamera.addCallbackBuffer(mBuffer);
mCamera.setPreviewCallbackWithBuffer(this);
// 输出数据怎么办
mCamera.startPreview();
// 因为用到了摄像头的宽高,所以写在这里
initEncode();
} catch (IOException e) {
e.printStackTrace();
}
}
private void initEncode() {
try {
mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
// 因为 摄像头数据 旋转了,所以这里的宽高就会变成高宽
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, mSize.height, mSize.width);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1080 * 1920);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
// 因为 YUV420 每个像素占用 3/2 个字节
int bufferLength = mSize.width*mSize.height*3/2;
nv12 = new byte[bufferLength];
mYuv420 = new byte[bufferLength];
} catch (IOException e) {
e.printStackTrace();
}
}
public int encodeFrame(byte[] input) {
// 因为摄像头的数据是 NV21,只有摄像头是这个格式,在硬件编码根本没有这个码
// 所以要转成YUV420
nv12 =YuvUtils.nv21toYUV420(input);
// 因为 摄像头是横着的,所以 数据也是横着的,把数据旋正
YuvUtils.portraitData2Raw(nv12, mYuv420, mSize.width, mSize.height);
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(100000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(mYuv420);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, mYuv420.length, System.currentTimeMillis(), 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
dealFrame(outputBuffer, bufferInfo);
// saveFile(outputBuffer,bufferInfo);
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
return 0;
}
public static final int NAL_I = 19;
public static final int NAL_VPS = 32;
private byte[] vps_sps_pps_buf;
private void dealFrame(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {
int offset = 4;
if (byteBuffer.get(2) == 0x01) {
offset = 3;
}
int type = (byteBuffer.get(offset) & 0x7E) >> 1;
// vps_sps_pps 帧记录下来
if (type == NAL_VPS) {
vps_sps_pps_buf = new byte[bufferInfo.size];
byteBuffer.get(vps_sps_pps_buf);
} else if (type == NAL_I) {
// I 帧 ,把 vps_sps_pps 帧塞到 I帧之前一起发出去
final byte[] bytes = new byte[bufferInfo.size];
byteBuffer.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);
sendData(newBuf);
Log.v(TAG, "I帧 视频数据 " + Arrays.toString(bytes));
} else {
// B 帧 P 帧 直接发送
final byte[] bytes = new byte[bufferInfo.size];
byteBuffer.get(bytes);
sendData(bytes);
}
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
encodeFrame(data);
mCamera.addCallbackBuffer(data);
}
private void saveFile(ByteBuffer buffer,MediaCodec.BufferInfo bufferInfo) {
byte[] bytes = new byte[bufferInfo.size];
buffer.get(bytes);
YuvUtils.writeBytes(bytes,mContext);
YuvUtils.writeContent(bytes,mContext);
}
}
YUV工具
主要是旋转工作
public class YuvUtils {
private static final String TAG = "YuvUtils";
static byte[] yuv420;
public static byte[] nv21toYUV420(byte[] nv21) {
int size = nv21.length;
yuv420 = new byte[size];
int len = size * 2 / 3;
System.arraycopy(nv21, 0, yuv420, 0, len);
int i = len;
while(i < size - 1){
yuv420[i] = nv21[i + 1];
yuv420[i + 1] = nv21[i];
i += 2;
}
return yuv420;
}
public static void portraitData2Raw(byte[] data,byte[] output,int width,int height) {
int y_len = width * height;
// uv数据高为y数据高的一半
int uvHeight = height >> 1;
int k = 0;
for (int j = 0; j < width; j++) {
for (int i = height - 1; i >= 0; i--) {
output[k++] = data[width * i + j];
}
}
for (int j = 0; j < width; j += 2) {
for (int i = uvHeight - 1; i >= 0; i--) {
output[k++] = data[y_len + width * i + j];
output[k++] = data[y_len + width * i + j + 1];
}
}
}
public static void writeBytes(byte[] array, Context context) {
FileOutputStream writer = null;
try {
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
writer = new FileOutputStream(context.getFilesDir() + "/codec.h265", true);
writer.write(array);
writer.write('\n');
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String writeContent(byte[] array, Context context) {
char[] HEX_CHAR_TABLE = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
StringBuilder sb = new StringBuilder();
for (byte b : array) {
sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);
sb.append(HEX_CHAR_TABLE[b & 0x0f]);
}
Log.i(TAG, "writeContent: " + sb.toString());
FileWriter writer = null;
try {
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
writer = new FileWriter(context.getFilesDir() + "/codecH265.txt", true);
writer.write(sb.toString());
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
还用到了 Java-WebSocket
记得在在每个app的buid.gradle中 同时 别忘了在清单文件中加 INTERNET 权限
implementation "org.java-websocket:Java-WebSocket:1.4.0"