###简介
注:因为是视频学习demo,在传输块没有做udp分包、并包处理,喂的帧数据不全导致了花屏等问题,有兴趣可以做分并包,关键字 40964。(2019.07.24
发送的是H264裸流到其它设备上解码显示,H264可见我整理的笔记:
ttp://blog.csdn.net/pds574834424/article/details/78150474
整体项目结构和使用方式如下:
先在手机上运行ScreenReceiveDemo接收APP,再在其它手机上运行主项目app并输入接收APP手机的IP地址。
###录屏发送
5.0开放了录屏API,在使用录屏时会用DialogAcitivty(部分奇葩ROM里没有此类会报错,建议处理catch)的形式提示用户是否授权。
主Activity如下:
private static final int ACTIVITY_RESULT_CODE = 110;
private MediaProjectionManager projectionManager;
private Context context;
private TextView main_demo_click_txt;
private VideoEncoderUtil videoEncoder;
private EditText main_demo_edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
context = this;
initView();
}
private void initView() {
projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
main_demo_edit = (EditText) findViewById(R.id.main_demo_edit);
main_demo_edit.setText("192.168.31.193");
main_demo_click_txt = (TextView) findViewById(R.id.main_demo_click_txt);
main_demo_click_txt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(main_demo_click_txt.getText().equals(context.getString(R.string.open_share))){
if(!NetworkUtil.ipCheck(main_demo_edit.getText().toString())){
ToastUtil.makeText(context, "大兄弟IP不对");
return;
}
Intent captureIntent = projectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, ACTIVITY_RESULT_CODE);
}else{
videoEncoder.stop();
main_demo_click_txt.setText(context.getString(R.string.open_share));
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ACTIVITY_RESULT_CODE && resultCode == RESULT_OK) {
MediaProjection mediaProjection = projectionManager.getMediaProjection(resultCode, data);
videoEncoder = new VideoEncoderUtil(mediaProjection, main_demo_edit.getText().toString());
videoEncoder.start();
main_demo_click_txt.setText(context.getString(R.string.close_share));
}
}
在onActivityResult拿到MediaProjectionManager管理下的MediaProjection,MediaProjection主要是用来授予应用程序捕获屏幕内容或记录系统音频的能力。
主要发送代码:
private class Encoder implements Runnable {
String MIME_TYPE = "video/avc";//编码格式, h264
int VIDEO_FRAME_PER_SECOND = 20;//fps
int VIDEO_I_FRAME_INTERVAL = 5;
private int mWidth = 1280;//大屏上会因为分辨率显示马赛克
private int mHeight = 720;
private int VIDEO_BITRATE = 2 * 1024 * 1024; //2M码率
// private int VIDEO_BITRATE = 500 * 1024; //500K码率,有兴趣的可以看看2M和500K的区别~
/**
* 子线程的hanlder
*/
private Handler threadHandler;
private DatagramSocket mDatagramSocket;
private MediaCodec mCodec;
private Surface mSurface;
private Bundle params = new Bundle();
Encoder() {
try {
if(mDatagramSocket == null){
mDatagramSocket = new DatagramSocket(null);
mDatagramSocket.setReuseAddress(true);
mDatagramSocket.bind(new InetSocketAddress(6666));
}
} catch (SocketException e) {
e.printStackTrace();
}
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);//做Bundle初始化 主要目的是请求编码器“即时”产生同步帧
prepare();
}
@Override
public void run() {
Looper.prepare();
threadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
byte[] dataFrame = (byte[]) msg.obj;
int frameLength = dataFrame.length;
byte[] lengthByte = TyteUtil.intToByteArray(frameLength);
byte[] concat = ArrayUtil.concat(lengthByte, dataFrame);
try {
DatagramPacket dp = new DatagramPacket(concat, concat.length, InetAddress.getByName(ip), 2333);
mDatagramSocket.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
}
};
Looper.loop();
}
void sendData(byte[] data) {
Message message = new Message();
message.obj = data;
threadHandler.sendMessage(message);
}
private void release() {
onSurfaceDestroyed(mSurface);
if (mCodec != null) {
mCodec.stop();
mCodec.release();
mCodec = null;
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean prepare() {
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
//COLOR_FormatSurface这里表明数据将是一个graphicbuffer元数据s
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BITRATE);//编码器需要, 解码器可选
format.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_FRAME_PER_SECOND);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, VIDEO_I_FRAME_INTERVAL);//帧间隔 这个参数在很多手机上无效, 第二帧关键帧过了之后全是P帧。 GOP实现还有其它方法,全局搜关键字GOP
try {
mCodec = MediaCodec.createEncoderByType(MIME_TYPE);
} catch (IOException e) {
e.printStackTrace();
return false;
}
mCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
if (index > -1) {
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
byte[] data = new byte[info.size];
assert outputBuffer != null;
outputBuffer.get(data);
sendData(data);
codec.releaseOutputBuffer(index, false);
}
if (System.currentTimeMillis() - timeStamp >= secondFrame) {//5秒后,设置请求关键帧的参数 GOP
timeStamp = System.currentTimeMillis();
codec.setParameters(params);
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
codec.reset();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
}
});
mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//创建关联的输入surface
mSurface = mCodec.createInputSurface();
mCodec.start();
onSurfaceCreated(mSurface, mWidth, mHeight);
return true;
}
关键方法onSurfaceBind把屏幕数据与编码器创建用来代替输入缓冲区的surface进行关联,在输出缓冲区onOutputBufferAvailable即可得到H264数据,注意GOP是在输出缓冲区做的。UDP的代码比较简单,写的也比较粗糙,主要是在前四位合并了一个数据总长度。如果接收方没有正常显示发送方的页面,请在接收方检查是否接收到数据。还是没数据的话,可以在电脑上用NetAssist软件检查,把屏幕发送的ip改成电脑ip尝试即可。
###解码显示
主要解码部分代码:
public void onFrame(byte[] buf) {
if (buf == null)
return;
int length = buf.length;
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();//拿到输入缓冲区,用于传送数据进行编码
//返回一个填充了有效数据的input buffer的索引,如果没有可用的buffer则返回-1.当timeoutUs==0时,该方法立即返回;当timeoutUs<0时,无限期地等待一个可用的input buffer;当timeoutUs>0时,至多等待timeoutUs微妙
// int inputBufferIndex = mediaCodec.dequeueInputBuffer(1);// =>0时,至多等待x微妙 如果发送源快速滑动比如放视频, 花屏明显.. ...
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);// <-1时,无限期地等待一个可用的input buffer 会出现:一直等待导致加载异常, 甚至会吃掉网络通道, 没有任何异常出现...(调试中大部分是因为sps和pps没有写入到解码器, 保证图像信息的参数写入解码器很重要)
if (inputBufferIndex >= 0) {//当输入缓冲区有效时,就是>=0
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(buf, 0, length);//往输入缓冲区写入数据,关键点
// int value = buf[4] & 0x0f;//nalu, 5是I帧, 7是sps 8是pps.
// if (value == 7)//如果不能保证第一帧写入的是sps和pps, 可以用这种方式等待sps和pps发送到之后写入解码器
// mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 30, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);//更新sps和pps
// else
mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 30, 0);//将缓冲区入队
mCount++;//用于queueInputBuffer presentationTimeUs 此缓冲区的显示时间戳(以微秒为单位),通常是这个缓冲区应该呈现的媒体时间
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);//拿到输出缓冲区的索引
// Log.e(TAG, "outputBufferIndex" + outputBufferIndex);
while (outputBufferIndex >= 0) {
mediaCodec.releaseOutputBuffer(outputBufferIndex, true);//显示并释放资源
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);//再次获取数据,如果没有数据输出则outIndex=-1 循环结束
}
}
解码显示比较简单,类里的备注也很多。注意需要优先初始化接收方,再发送,因为解码器需要喂入相应的sps和pps参数再加上关键帧才能达到理论上的秒开效果,并且不优先喂入此数据,会产生黑屏,花屏等异常。UDP的接收长度是写死的,前面四位是数据总长度,后面数据是实际的H264数据。
###DEMO代码