我正在尝试在没有 STUN 或 TURN 服务器的私有网络上创建一个从 android 相机(使用 kotlin)到 web 浏览器(chrome)的单向视频 webRTC。浏览器调用 android,当 android 应答时,它应该将 android 视频流发送到浏览器服务器,在那里它可以通过视频元素显示。浏览器不需要向 android 发送任何视频或音频。 问题是 android 没有生成任何 ICE 候选人。它创建一个答案,并设置本地描述但不创建或发送任何 ICE 候选人。他们交换描述,并在 chrome://webrtc-internals 中表示 SDP 连接稳定。
Android answer() 函数:
fun answer(target: String) {
Log.d("answer","xxx answer: $target")
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
}
peerConnection?.createAnswer(object : SdpObserver {
override fun onCreateSuccess(desc: SessionDescription?) {
Log.d("onCreateSuccess", "xxx onCreateSuccess")
peerConnection?.setLocalDescription(object : SdpObserver {
override fun onCreateSuccess(p0: SessionDescription?) {
Log.d("answer success", "xxx local description created")
}
override fun onSetSuccess() {
Log.d("onSetSuccess", "xxx onSetSuccess")
// print the localDescription
Log.d("localDescription", "xxx ${peerConnection?.localDescription}")
val answer = hashMapOf(
"sdp" to desc?.description,
"type" to desc?.type
)
socketRepository.sendMessageToSocket(
MessageModel(
"create_answer", username, target, answer
)
)but android isn’t generating ice candidates for some reason,
}
override fun onCreateFailure(p0: String?) {
Log.d("xxx onCreateFailure", "$p0")
}
override fun onSetFailure(p0: String?) {
Log.d("xxx onSetFailure", "$p0")
}
}, desc) // this line means that we are passing the desc object to the onCreateSuccess method of the SdpObserver object we created above
}
override fun onSetSuccess() {
}
override fun onCreateFailure(p0: String?) {
Log.d("xxx onCreateFailure", "$p0")
}
override fun onSetFailure(p0: String?) {
Log.d("xxx onSetFailure", "$p0")
}
}, constraints)
}
浏览器服务器:
function handleNegotiationNeededEvent() {
console.log("handleNegotiationNeededEvent")
peerConnection
.createOffer(offer_constraints)
.then((offer) => peerConnection.setLocalDescription(offer))
.then(async () => {
await waitUntilIceGatheringStateComplete(peerConnection, options);
console.log('offer', peerConnection.localDescription)
client.send(JSON.stringify({
type: "create_offer",
name: peer2,
target: peer1,
data: peerConnection.localDescription
}))
})
.catch((e) => {
console.log(e)
});
}
浏览器服务器消息切换语句:
client.onmessage = async function (e) {
const data = JSON.parse(e.data)
console.log(data);
switch (data.type) {
case "call_response":
if (data.data == "user is ready for call") {
// 3. Create a new RTCPeerConnection for broadcaster
createPeerConnection();
peerConnection.addTransceiver('video', { direction: 'recvonly' })
videoTrack = peerConnection.getTransceivers()[0].receiver.track;
}
break;
case "answer_received":
if (data.name == peer1) {
console.log('answer received', data)
await peerConnection.setRemoteDescription({
type: "answer",
sdp: data.data
})
const videos = document.createElement('div');
videos.className = 'grid';
document.body.appendChild(videos);
const localVideo = document.createElement('video');
localVideo.muted = true;
localVideo.autoplay = true;
localVideo.height = 240;
localVideo.width = 320;
localVideo.id = 'vid_player'
videos.appendChild(localVideo);
const mediaStream = new MediaStream(peerConnection.getReceivers().map(receiver => receiver.track))
console.log('mediaStream', mediaStream)
let vid_player = document.getElementById('vid_player')
vid_player.srcObject = mediaStream
}
break;
case "ice_candidate":
if (data.name == peer1) {
console.log('ice candidate received', data)
// create new ice candidate and add it to our peer connection
const candidate = new RTCIceCandidate({
sdpMLineIndex: data.data.sdpMLineIndex,
sdpMid: data.data.sdpMid,
candidate: data.data.sdpCandidate,
})
await peerConnection.addIceCandidate(candidate).catch(e => console.error(e));
}
}
}