技术标签: 音视频
接着之前写的 音视频系列–MediaProjection+MediaCodec制作简单投屏效果,继续使用Camera2+MediaCodec
来制作相互实时投屏效果,为后面的直播学习打下基础。
模拟器作为客户端,手机作为服务端,用模拟器实时接收的手机投屏数据,如果是两个手机可以相互投屏。
整个过程使用Camera2
来获取数据,获取的数据通过MediaCodec
编码,使用H264
协议,传输通过WebSocket
来实现,接收端接收到数据之后解码,然后通过TextureView渲染,大概是这么一个流程,下面记录下实现过程。
public class MainActivity extends AppCompatActivity implements SocketLive.SocketCallback{
...
//远程服务端的TextureView初始化
private void initRemoteTextureView() {
remoteTextureView = findViewById(R.id.remoteTextureView);
remoteTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Surface remoteSurface = new Surface(surface);
decodePlayerLiveH264 = new DecodePlayerLiveH264();
decodePlayerLiveH264.initDecoder(remoteSurface);
}
...
});
}
//请求相互连接模拟
public void connect(View view) {
localTextureView.startCapture(this);
}
//onResume 打开本地相机
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
localTextureView.openCamera();
}
}
//onPause关闭本地相机
@Override
protected void onPause() {
super.onPause();
localTextureView.closeCamera();
}
//onDestroy资源释放
@Override
protected void onDestroy() {
super.onDestroy();
localTextureView.onDestroy();
}
...
//接收端接收到另一端的数据之后的回调,传给解码器解码,然后交给TextureView渲染
@Override
public void callBack(byte[] data) {
if (decodePlayerLiveH264 != null) {
decodePlayerLiveH264.callBack(data);
}
}
}
private void initializeCameraManager() {
//由于后面相机的参数需要传一个Handler,指明回调在那个线程,所以提前初始化
startBackgroundThread();
mCameraManager = (CameraManager) this.mContext.getSystemService(Context.CAMERA_SERVICE);
try {
//获取可用的相机列表
String[] cameraIdList = mCameraManager.getCameraIdList();
for (String cameraId : cameraIdList) {
//获取该相机的CameraCharacteristics,它保存的相机相关的属性
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
//获取相机的方向
Integer facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
//如果是前置摄像头就continue,我们这里只用后置摄像头
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
continue;
}
//保存cameraId
this.mCameraId = cameraId;
this.mBackCameraCharacteristics = cameraCharacteristics;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void openCamera() {
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
try {
prepareCameraOutputs();
if (isAvailable()) {
mCameraManager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} else {
setSurfaceTextureListener(this);
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
protected void prepareCameraOutputs() {
//获取相机支持的流的参数的集合
StreamConfigurationMap map = mBackCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//寻找一个 最合适的尺寸
mPreviewSize = getBestSupportedSize(new ArrayList<Size>(Arrays.asList(map.getOutputSizes(SurfaceTexture.class))));
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.YUV_420_888, 2);
mImageReader.setOnImageAvailableListener(this, mBackgroundHandler);
}
在打开相机之前先,先要去获取相机支持的最接近的预览尺寸,然后要实例化一个ImageReader
,用来接收Camera2
的可用数据。这些做完之后,需要监测TextureView
是否可用,不可用先设置setSurfaceTextureListener
监听,可用之后再来打开相机。
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
Camera2TextureView.this.mCameraDevice = cameraDevice;
createCaptureSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
}
};
private void createCaptureSession() {
try {
mSurfaceTexture = getSurfaceTexture();
mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mWorkingSurface = new Surface(mSurfaceTexture);
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//添加接收数据回调的Target Surface
mPreviewRequestBuilder.addTarget(mWorkingSurface);
mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(mWorkingSurface, mImageReader.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
updatePreview(cameraCaptureSession);
}
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
Log.d(TAG, "Fail while starting preview: ");
}
}, null);
} catch (Exception e) {
Log.e(TAG, "Error while preparing surface for preview: ", e);
}
}
private void updatePreview(CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) {
return;
}
mCaptureSession = cameraCaptureSession;
//设置自动对焦模式为连续自动对焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//创建CaptureRequest
mPreviewRequest = mPreviewRequestBuilder.build();
try {
//开始预览
mCaptureSession.setRepeatingRequest(mPreviewRequest, null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public void onImageAvailable(ImageReader reader) {
Image image= reader.acquireNextImage();
Image.Plane[] planes = image.getPlanes();
// 重复使用同一批byte数组,减少gc频率
if (y == null) {
y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
}
if (image.getPlanes()[0].getBuffer().remaining() == y.length) {
planes[0].getBuffer().get(y);
planes[1].getBuffer().get(u);
planes[2].getBuffer().get(v);
}
if (nv21 == null) {
// 实例化一次
nv21 = new byte[planes[0].getRowStride() * mPreviewSize.getHeight() * 3 / 2];
nv21_rotated = new byte[planes[0].getRowStride() * mPreviewSize.getHeight() * 3 / 2];
}
ImageUtil.yuvToNv21(y, u, v, nv21, planes[0].getRowStride(), mPreviewSize.getHeight());
ImageUtil.nv21_rotate_to_90(nv21, nv21_rotated, planes[0].getRowStride(), mPreviewSize.getHeight());
byte[] temp = ImageUtil.nv21toNV12(nv21_rotated, nv12);
if(encodePushLiveH264 != null){
encodePushLiveH264.startLive();
encodePushLiveH264.encodeFrame(temp);
}
//注意这里用完之后要关闭
image.close();
}
由于Camera2的yuv数据获取到数据需要自己手动拼接,所以这里是和Camera1不同的,获取到的数据拼接成nv21,需要转化成nv12,因为MediaCodec不支持nv21。
///摄像头调用
public int encodeFrame(byte[] input) {
int inputBufferIndex = mediaCodec.dequeueInputBuffer(100000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(input);
long presentationTimeUs = computePresentationTime(frameIndex);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.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;
}
private void dealFrame(ByteBuffer bb, MediaCodec.BufferInfo bufferInfo) {
int offset = 4;
if (bb.get(2) == 0x01) {
offset = 3;
}
int type = bb.get(offset) & 0x1F;
if (type == NAL_SPS) {
sps_pps_buf = new byte[bufferInfo.size];
bb.get(sps_pps_buf);
} else if (type == NAL_I) {
final byte[] bytes = new byte[bufferInfo.size];
bb.get(bytes);
byte[] newBuf = new byte[sps_pps_buf.length + bytes.length];
System.arraycopy(sps_pps_buf, 0, newBuf, 0, sps_pps_buf.length);
System.arraycopy(bytes, 0, newBuf, sps_pps_buf.length, bytes.length);
this.socketLive.sendData(newBuf);
} else {
final byte[] bytes = new byte[bufferInfo.size];
bb.get(bytes);
this.socketLive.sendData(bytes);
}
}
1.h264中帧类型type
在后位,所以与上0x1F
获取后5位,通过这个type
来获取帧类型。
2.上面数据处理的时候,对于sps
直接保留下来就行了,I帧
数据前面需要拼接sps
,其它的就直接通过websocket
传输。
WebSocket
配置
implementation "org.java-websocket:Java-WebSocket:1.4.0"
1.WebSocket配置
private WebSocketServer webSocketServer = new WebSocketServer(new InetSocketAddress(配置端口,设置高一点,比如12001)) {
@Override
public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) {
SocketLive.this.webSocket = webSocket;
Log.i("TAG", "onOpen: 服务端 打开 socket ");
}
@Override
public void onClose(WebSocket webSocket, int i, String s, boolean b) {
Log.i(TAG, "onClose: 关闭 socket ");
}
@Override
public void onMessage(WebSocket webSocket, String s) {
}
//接收到对端数据然后解码渲染
@Override
public void onMessage(WebSocket conn, ByteBuffer bytes) {
byte[] buf = new byte[bytes.remaining()];
bytes.get(buf);
socketCallback.callBack(buf);
}
@Override
public void onError(WebSocket webSocket, Exception e) {
Log.i(TAG, "onError: " + e.toString());
}
@Override
public void onStart() {
}
};
2.开启和关闭
public void start() {
webSocketServer.start();
}
public void close() {
try {
webSocket.close();
webSocketServer.stop();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.传输数据
public void sendData(byte[] bytes) {
if (webSocket != null && webSocket.isOpen()) {
Log.i("TAG", "sendData: " + Arrays.toString(bytes));
webSocket.send(bytes);
}
}
1.WebSocket配置
private class MyWebSocketClient extends WebSocketClient {
public MyWebSocketClient(URI serverURI) {
super(serverURI);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
Log.i(TAG, "打开 socket onOpen: ");
}
@Override
public void onMessage(String s) {
}
//接收到对端数据
@Override
public void onMessage(ByteBuffer bytes) {
//remaining是实际的数据长度
byte[] buf = new byte[bytes.remaining()];
bytes.get(buf);
socketCallback.callBack(buf);
}
@Override
public void onClose(int i, String s, boolean b) {
Log.i(TAG, "onClose: ");
}
@Override
public void onError(Exception e) {
Log.i(TAG, "onError: ");
}
}
public interface SocketCallback{
void callBack(byte[] data);
}
2.WebSocket启动和连接
public void start() {
try {
//两个机子连同一个网络
URI url = new URI("ws://xxx.xxx.xxx.xxx:12001");
myWebSocketClient = new MyWebSocketClient(url);
myWebSocketClient.connect();
} catch (Exception e) {
e.printStackTrace();
}
}
public void callBack(byte[] data) {
int index= mediaCodec.dequeueInputBuffer(100000);
if (index >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(index);
inputBuffer.clear();
inputBuffer.put(data, 0, data.length);
//dsp芯片解码 解码 的 传进去的 只需要保证编码顺序就好了 1000
mediaCodec.queueInputBuffer(index,
0, data.length, System.currentTimeMillis(), 0);
}
// 获取到解码后的数据 编码 ipbn
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
while (outputBufferIndex >=0) {
mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
文章浏览阅读100次。6.3.6 安装并更新CsDatabase好了,打开Skype For Business命令行管理工具重启下前端服务看看是否能够成功启动,我这里直接重新启动服务器了 转载于:https://blog.51cto.com/winteragain/1703803..._skype for business 2015如何打累计更新
文章浏览阅读492次。HLS编程环境入门_hls verilog
文章浏览阅读149次。点上方蓝字计算机视觉联盟获取更多干货在右上方···设为星标★,与你不见不散编辑:Sophia计算机视觉联盟 报道 |公众号CVLianMeng转载于 :中国石油大学她,大..._哈工大博士32篇sci
文章浏览阅读5.1k次。原文:mysql安装出现应用程序无法正常启动(oxc000007b)的解决方案 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/IUNIQUE/article/details/82864859 有时候安装m..._安装mysql时电脑出现此应用无法在你的电脑上运行怎么办
文章浏览阅读43次。在XenApp5.0中,策略中并没有提供对USB设备的映射。不过官方提供了如下方案:新建如下键值:OnXenApp32-bitEditionHKEY_LOCAL_MACHINE\Software\Citrix\Policies\DisableUSBDriveRedirectionOnXenApp64-bitEditionHKEY_LOCAL_MACHINE\So..._citrix xenapp 5.0映射客户端优盘
文章浏览阅读538次。图像的高级处理中,协方差矩阵计算是必不可少的,但opencv关于这方面的资料却相当少。首先,利用matlab计算一下,便于比较:>> data=[1,2,3;10,20,30]data =1 2 310 20 30>> convar=cov(data)convar =40.5000 81.0000 121.500081.0000 162...._图像的协方差矩阵计算过程
文章浏览阅读698次,点赞2次,收藏3次。1. 小程序功能古诗词大全成语大全成语接龙诗词飞花令诗词分享、收藏诗词接龙唐诗宋词起名字百家姓猜谜语2. 小程序地址https://github.com/caochangkui/miniprogram-project3. 小程序预览:4. 部分截图首页列表页详情页 分享页唐诗宋词成语接龙5. 项目结构.├── README.md├──..._成语接龙云开发数据库
文章浏览阅读452次。2018-2022是私有云混合云在中国最火热的时代,私有云将在中国从摸索走向成熟阶段,随着云技术的火热,下一个企业必须要思考的将是信息安全的问题,现在企业都在导入云计算技术,建置更多的信息应用系统以从中获取信息化带来的价值。那么随着带来的一个隐患就是,管理员要管理的基础架构和应用系统数量越来越多,这时候管理员账户就变的很重要了,如何保证管理员账户能够安全,如果保证管理员账户的..._-membertimetolive
文章浏览阅读90次。“Service Mesh要解决分布式架构下如何集成的问题,同时它又是云原生的核心,Dubbo Mesh正在做这方面的实践。--- 阿里巴巴Apache Dubbo布道师 吕仁琦”本文整理自2018杭州云栖大会首届开发者生态峰会吕仁琦的分享。- 公众号后台发送“首届开发者生态峰会”,获取峰会PPT。| Service Mesh 和 Du..._apache dubbo 与 alibaba dubobo site:blog.csdn.net
文章浏览阅读1.3k次。《偏微分方程数值解法MATLAB源码》由会员分享,可在线阅读,更多相关《偏微分方程数值解法MATLAB源码(27页珍藏版)》请在人人文库网上搜索。1、源码【更新完毕】偏微分方程数值解法的MATLAB原创 说明:由于偏微分的程序都比较长,比其他的算法稍复杂一些,所以另开一贴,专门上传偏微分的程序 谢谢大家的支持! 其他的数值算法见:./Announce/Announce.asp?BoardID=20..._在matlab中使用crank-nicolson 方法求解偏微分方程
文章浏览阅读6.9k次,点赞5次,收藏17次。一、Flink 基本概念Flink 是一个批处理和流处理结合的统一计算框架,其核心是一个提供了数据分发以及并行化计算的流数据处理引擎。它的最大亮点是流处理,是业界最顶级的开源流处理引擎。Flink 与 Storm 类似,属于事件驱动型实时流系统。所谓说事件驱动型指的就是一个应用提交之后,除非明确的指定停止,否则,该业务会一直持续的运行,它的执行条件就是触发了某一个事件,比如在淘宝中,我..._批处理和流计算
文章浏览阅读3.6k次。在Windows2000/XP中,我们一般会用到故障恢复控制台集成的一些增强命令,比如Fixmbr用于修复和替换指定驱动器的主引导记录、Fixboot用于修复知道驱动器的引导扇区、Diskpart能够增加或者删除硬盘中的分区、Expand可以从指定的CAB源文件中提取出丢失的文件、Listsvc可以创建一个服务列表并显示出服务当前的启动状态、Disable和Enable分别用于禁止和允许一项服务或者硬件设备等等,而且输入“help”命令可以查看到所有的控制命令以及命令的详细解释。......_调整分区容量时出现错误