Webrtc 实现Android群控实时推流

云手机同步效果

现在市面上云手机 基本都是基于arm服务器+docker容器技术来实现,其中最核心的是gpu虚拟化,然后就是 远程画面操作。

今天和大家分享下如何在Android上把视频画面以及音频通过webrtc传输到客户端或者pc上,以及如何实现远程遥控操

1.首先我们需要一个信令服务用于转发信令消息,在推流与收流端正常通讯后。使用webrtc发送音视频,其中nat服务器使用的是cotrun。

2.在推流端实现推流关键代码如下

 private void start() {
        videoCapturer = createScreenCapturer();
        if (videoCapturer == null) {
            return;
        }

        eglBaseContext = EglBase.create().getEglBaseContext();

        surfaceViewRendererLocal.init(eglBaseContext, null);
        surfaceViewRendererLocal.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        surfaceViewRendererLocal.setKeepScreenOn(true);

        surfaceViewRendererRemote.init(eglBaseContext, null);
        surfaceViewRendererRemote.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        surfaceViewRendererRemote.setKeepScreenOn(true);

        peerConnectionFactory = createPeerConnectionFactory();

        //使用硬件
        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
        DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(eglBaseContext);
        DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory(eglBaseContext, true, true);
        peerConnectionFactory = PeerConnectionFactory.builder().setOptions(options).setVideoDecoderFactory(defaultVideoDecoderFactory).setVideoEncoderFactory(defaultVideoEncoderFactory).createPeerConnectionFactory();


        MediaConstraints audioConstraints = new MediaConstraints();
//        //回声消除
//        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
//        //自动增益
//        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
//        //高音过滤
//        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
//        //噪音处理
//        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
        AudioSource audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
        audioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);

        surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
        VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
        videoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());

        videoTrack = peerConnectionFactory.createVideoTrack("102", videoSource);
        videoTrack.addSink(surfaceViewRendererLocal);

        btnCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                call();
            }
        });

        webSocketChannel = new WebSocketChannel();
        webSocketChannel.setWebSocketCallback(webSocketCallback);
        webSocketChannel.connect(WEBSOCKET_URL);

    }

3 收流端采用html我试过用腾讯的x5直接加载html也可

<!DOCTYPE html>
<html>
<head>
    <title>webrtc demo</title>
</head>
<!--去除白边-->
<body style="border:0;margin:0;padding:0;">

<div>

    <div style="position:absolute;z-index:321;left:50%">
        <button style="background:#F00; color:#FFF" onclick="makeCall()">申请实例</button>
    </div>

    <div style="position:absolute;z-index:321;left:20%;top:10%">
        <button style="background:#F00; color:#FFF" onclick="sendText()">点击一下通知云手机</button>
    </div>

    <video id="remoteVideo" autoplay playsinline controls="false"
           style="width: 100%;height: 100%;object-fit: fill;position:relative;"/>

</div>
<script>

		// 官网 https://webrtc.org/getting-started/peer-connections

		const LOCAL_USER_ID = 'bbbb';
		const RECEIVER_USER_ID = 'aaaa';

		var webSocket = new WebSocket('ws://221.7.79.213:5000/websocket');
		webSocket.onerror = function(event) {
			console.log('open error');
		};
		webSocket.onopen = function(event) {
			console.log("onopen---------->>>>fuck");
			// 注册自己到服务端
			webSocket.send(JSON.stringify({
				'event':'register',
				'userId': LOCAL_USER_ID
			}));
		};
		webSocket.onclose = function(event) {
			console.log('websocket closed');
		};
		webSocket.onmessage = function(event) {
			console.log('onmessage '+event.data);
			// 回调
			onReceiveSocketMessage(JSON.parse(event.data));
		};

       // ice 服务器列表
		const configuration = {'iceServers': [
		{'urls': 'stun:stun.l.google.com:19302'},
		{'urls': 'stun:42.192.40.58:3478?transport=udp'},
		{'urls': 'turn:42.192.40.58:3478?transport=udp','username':'ddssingsong','credential':'123456'},
		{'urls': 'turn:42.192.40.58:3478?transport=tcp','username':'ddssingsong','credential':'123456'},
		{'urls': 'turn:221.7.79.213:3478?transport=udp','username':'admin','credential':'123456'},
		]}
		// 管道
		var peerConnection = new RTCPeerConnection(configuration); // 全局 PeerConnection
		var dataChannel;//datachannel 发送消息

		// Listen for local ICE candidates on the local RTCPeerConnection
		peerConnection.addEventListener('icecandidate', event => {
			console.log('local trickle  '+event);
			if (event.candidate) {
				const trickle = {
					event:'trickle',
					sender:LOCAL_USER_ID,
					receiver: RECEIVER_USER_ID,
					candidate:{
						sdpMid:event.candidate.sdpMid,
						sdpMLineIndex:event.candidate.sdpMLineIndex,
						sdp:event.candidate.candidate
					}
				};

				webSocket.send(JSON.stringify(trickle));
				// 发送本地 ICE candidate 到远端
				console.log('send local ice candidate to remote '+RECEIVER_USER_ID);
			}
		});
		// Listen for connectionstatechange on the local RTCPeerConnection
		peerConnection.addEventListener('connectionstatechange', event => {
			if (peerConnection.connectionState === 'connected') {
				// Peers connected! 会话连接完成
				console.log('p2p connected');
				// 初始化管道
				initDataChanel();
			}
		});

		// 处理java 的 signaling 消息
		async function onReceiveSocketMessage(message) {
			if ('sdp'===message.event) {
				if('offer'==message.type.toLowerCase()) {
					// 收到 offer
					console.log('收到 offer');
					let offerSdp = {type:'offer', sdp: message.description};
					peerConnection.setRemoteDescription(new RTCSessionDescription(offerSdp));
					// 创建 answer
					const answer = await peerConnection.createAnswer();
					await peerConnection.setLocalDescription(answer);
					console.log('create answer and setLocalDescription');
					// 发送 answer 到对端
					webSocket.send(JSON.stringify({
						"event": "sdp",
						"type": 'answer',
						"description":answer.sdp,
						"sender":LOCAL_USER_ID,
						"receiver":message.sender
					}));
					console.log('send answer to remote '+message.receiver);
				} else if ('answer'==message.type.toLowerCase()) {
					// 收到 answer
					console.log('收到 answer from '+message.sender);
					let answerSdp = {type:'answer', sdp: message.description};
					const remoteDesc = new RTCSessionDescription(answerSdp);
					await peerConnection.setRemoteDescription(remoteDesc);
					console.log('setRemoteDescription after reveiver answer');
				}
			} else if('trickle'===message.event) {
				let candidate = message.candidate;
				await peerConnection.addIceCandidate(new RTCIceCandidate({
					sdpMid:candidate.sdpMid,
					sdpMLineIndex:candidate.sdpMLineIndex,
					candidate:candidate.candidate
				}))
				console.log('add remote candidate');
			}
		};

		async function addStreams() {
			const remoteStream = new MediaStream();
			const remoteVideo = document.querySelector('video#remoteVideo');
			remoteVideo.srcObject = remoteStream;
			// 监听远程流
			peerConnection.addEventListener('track', async (event) => {
		      	console.log('监听远程流');
				remoteStream.addTrack(event.track, remoteStream);
			});
		}

		// 添加流
		addStreams();

		// 拨打
		async function makeCall() {
			// create offer
			const offer = await peerConnection.createOffer();
			console.log('create offer');
			console.log(offer);
			await peerConnection.setLocalDescription(offer);
			console.log('set setLocalDescription offer');

			// 发送 offer 到对端
			webSocket.send(JSON.stringify({
				"event": "sdp",
				"type": 'offer',
				"description":offer.sdp,
				"sender":LOCAL_USER_ID,
				"receiver":RECEIVER_USER_ID
			}));
			console.log('send offer to remote '+RECEIVER_USER_ID);

		}

	// 状态改变
     function dataChannelStateChange(){
	     var readyState=dataChannel.readyState;
         console.log(' will send ttttttttt:'+readyState);
	     if(readyState==='open'){
              sendText();
	     }else{

	     }
     }
     function dataChannelError(error){
	    console.log("Data Channel Error:", error);
     }

     //文本对方传过来的数据
    function reveivemsg(e){
    	var msg = e.data;
    	console.log('recreived msg is :'+e.data);
    	if(msg){
    		chat.value += '->' + msg + '\r\n';
    	}else{
    		console.error('recreived msg is null');
    	}
    }

	// 初始化datachannel
    function initDataChanel(){
        var dataChannelOptions = {
	      ordered: true, //保证到达顺序
	    };
	    //文本聊天
	    dataChannel=peerConnection.createDataChannel('dataChannel',dataChannelOptions);
	    dataChannel.onmessage=reveivemsg; //收到消息
	    dataChannel.onopen = dataChannelStateChange; //状态改变
	    dataChannel.onclose = dataChannelStateChange; //
	    dataChannel.onerror = dataChannelError; //错误

    }


    //发送文本
    function sendText(){
    		dataChannel.send('ttttttttt');
    		console.log('send ttttttttt');
    }




</script>

</body>
</html>

当然原生的Android客户端也可以的

原生的Java收流端类似推流端 只是不发送本地音视频

以上就是公网的实时效果,由于时间限制所以没有录制视频 

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
一: 使用javacv来实现,最终也是用过ffmpeg来进行编码和推流,javacv实现到可以直接接收摄像头的帧数据 需要自己实现的代码只是打开摄像头,写一个SurfaceView进行预览,然后实现PreviewCallback将摄像头每一帧的数据交给javacv即可 javacv地址:https://github.com/bytedeco/javacv demo地址:https://github.com/beautifulSoup/RtmpRecoder/tree/master 二: 使用Android自带的编码工具,可实现硬编码,这里有一个国内大神开源的封装很完善的的库yasea,第一种方法需要实现的Camera采集部分也一起封装好了,进行一些简单配置就可以实现编码推流,并且yasea目前已经直接支持摄像头的热切换,和各种滤镜效果 yasea地址(内置demo):https://github.com/begeekmyfriend/yasea 服务器 流媒体服务器我用的是srs,项目地址:https://github.com/ossrs/srs 关于srs的编译、配置、部署、在官方wiki中已经写的很详细了,并且srs同样是国内开发人员开源的项目,有全中文的文档,看起来很方便 这里有最基本的简单编译部署过程 Android直播实现(二)srs流媒体服务器部署 播放器 android端的播放使用vitamio,还是国内的开源播放器,是不是感觉国内的前辈们越来越屌了^~^! vitamio支持几乎所有常见的的视频格式和流媒体协议 vitamio地址(内置demo):https://github.com/yixia/VitamioBundle 这里使用的是yaesa库,先介绍一下直播实现的流程:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值