Соединение WebRTC ICE не выполнено с сообщением «Срок действия согласия на отправку истек»Python

Программы на Python
Ответить Пред. темаСлед. тема
Anonymous
 Соединение WebRTC ICE не выполнено с сообщением «Срок действия согласия на отправку истек»

Сообщение Anonymous »

Я работаю над проектом, в котором мне нужно установить соединение WebRTC между клиентом C# WPF (с использованием .NET Core) и сервером Python FastAPI. Цель состоит в том, чтобы обеспечить однонаправленный видеопоток от сервера к клиенту.
Сервер использует aiortc для потоковой передачи синтетического видеопотока, а клиент создан с помощью SIPSorcery для получить поток. Однако после первоначального обмена кандидатами ICE и ответа SDP я постоянно вижу сбой соединения с сообщением «

Код: Выделить всё

Consent to send expired
" на стороне сервера через некоторое время, и клиент сразу же меняет состояние соединения на неудачное.
Минимальное количество воспроизводимых примеров
< h2>Клиентский код:

Код: Выделить всё

using Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Text;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using SIPSorcery.Net;
using SIPSorceryMedia.Encoders;
using SIPSorcery.Media;

namespace Iris.Services
{
public class IPCameraService
{
private ClientWebSocket ws = new ClientWebSocket();  // WebSocket for communication
private RTCPeerConnection pc;
private string wsUrl = "ws://your.websocket.url";  // Replace with actual WebSocket URL
private ConcurrentDictionary CameraWebsockets = new ConcurrentDictionary();

public async Task Main(CancellationToken token)
{
Debug.WriteLine("Starting WebRTC connection setup.");
await StartSocket(token);
await EstablishConnection(token);
await SendOfferWithCandidates(token);
_ = Task.Run(() =>  ReceiveMessages(token));
Debug.WriteLine("WebRTC connection setup complete.");
}

public async Task StartSocket(CancellationToken token)
{
if (ws.State != WebSocketState.Open)
{
ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(wsUrl), token);
CameraWebsockets[0] = ws;
Debug.WriteLine("WebSocket connected.");
}
}

public async Task EstablishConnection(CancellationToken token)
{
Debug.WriteLine("Establishing PeerConnection.");
pc = new RTCPeerConnection();

// Log ICE connection state changes
pc.oniceconnectionstatechange += (state) =>
{
Debug.WriteLine($"ICE connection state changed to: {state}");
};
pc.onconnectionstatechange += (state) =>
{
Debug.WriteLine($"Connection state changed to {state}");
};

// Initialize a dummy video source
var testPatternSource = new VideoTestPatternSource(new VpxVideoEncoder());
MediaStreamTrack videoTrack = new MediaStreamTrack(testPatternSource.GetVideoSourceFormats(), MediaStreamStatusEnum.SendRecv);
pc.addTrack(videoTrack);
Debug.WriteLine("Video track added to PeerConnection.");
}

public async Task SendOfferWithCandidates(CancellationToken token)
{
Debug.WriteLine("Creating SDP offer.");
var offer = pc.createOffer();
await pc.setLocalDescription(offer);
Debug.WriteLine($"SDP Offer created:\n{offer.sdp}");

var offerMessage = new
{
sdp = offer.sdp,
type = "offer"
};
await SendMessage(offerMessage, token);
Debug.WriteLine("SDP Offer sent to server.");

pc.onicecandidate += async (RTCIceCandidate candidate) =>
{
if (candidate != null)
{
Debug.WriteLine($"ICE candidate gathered:\n{candidate}");
var candidateMessage = new { command = "Ice_Candidate", candidate };
await SendMessage(candidateMessage, token);
Debug.WriteLine("ICE candidate sent to server.");
}
};
}

private async Task SendMessage(object message, CancellationToken token)
{
var ws = CameraWebsockets[0];
var messageJson = JsonConvert.SerializeObject(message);
var messageBytes = Encoding.UTF8.GetBytes(messageJson);
await ws.SendAsync(new ArraySegment(messageBytes), WebSocketMessageType.Text, true, token);
Debug.WriteLine($"Message sent: {messageJson}");
}

public async Task ReceiveMessages(CancellationToken token)
{
var ws = CameraWebsockets[0];
var buffer = new ArraySegment(new byte[8192]);

while (ws.State == WebSocketState.Open && !token.IsCancellationRequested)
{
var result = await ws.ReceiveAsync(buffer, token);
string response = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);
var jsonResponse = JsonConvert.DeserializeObject(response);
Debug.WriteLine($"Received message from server: {response}");

// Handle SDP Answer
if (jsonResponse.ContainsKey("sdp"))
{
var sdpAnswer = jsonResponse["sdp"].ToString();
var sdpType = jsonResponse["type"].ToString();
Debug.WriteLine($"Received SDP Answer:\n{sdpAnswer}");
var sdp = SDP.ParseSDPDescription(sdpAnswer);
pc.SetRemoteDescription(sdpType == "answer"  ? SdpType.answer : SdpType.offer, sdp);
Debug.WriteLine("SDP Answer set on PeerConnection.");
}
// Handle ICE Candidates
else if (jsonResponse.ContainsKey("candidate"))
{
var candidate = jsonResponse["candidate"].ToString();
Debug.WriteLine($"Received ICE Candidate:\n{candidate}");
var iceCandidate = new RTCIceCandidateInit { candidate = candidate };
pc.addIceCandidate(iceCandidate);
Debug.WriteLine("ICE Candidate added to PeerConnection.");
}
}
}
}
}
Код сервера:

Код: Выделить всё

from fastapi import APIRouter, WebSocket
import json
import asyncio
import logging
from starlette.websockets import WebSocketDisconnect
from aiortc import VideoStreamTrack, RTCPeerConnection, RTCSessionDescription, RTCIceCandidate
from aiortc.mediastreams import VideoFrame
from fractions import Fraction
import time
import numpy as np

logger = logging.getLogger("uvicorn")

class SyntheticVideoTrack(VideoStreamTrack):
def __init__(self, fps=15, width=1280, height=720, color=(0, 255, 0)):
super().__init__()
self.width = width
self.height = height
self.fps = fps
self.color = color  # RGB color for the synthetic frame background
self.frame_interval = 1.0 / fps
self.start_time = time.time()  # Track the start time for PTS calculation

async def recv(self):
"""Generate synthetic frames at a consistent frame rate."""
await asyncio.sleep(self.frame_interval)

# Create a solid color frame
frame_data = np.full((self.height, self.width, 3), self.color, dtype=np.uint8)
video_frame = VideoFrame.from_ndarray(frame_data, format="rgb24")

# Set presentation timestamp (PTS) and time base
elapsed_time = time.time() - self.start_time
video_frame.pts = int(elapsed_time * 90000)  # PTS in 90kHz clock
video_frame.time_base = Fraction(1, 90000)

return video_frame

class VideoProcessor:
def __init__(self):
self.pc = None  # Peer connection

async def websocket_control(self, websocket: WebSocket):
logger.info("WebSocket Server Started")

while True:
try:
data = await websocket.receive_text()
message = json.loads(data)
logger.info(f"Received message: {message}")

if message["command"] == "Start_Stream":
camera_url = message.get("camera_url")
if camera_url:
logger.info(f"Starting WebRTC stream with camera URL: {camera_url}")
await self.start_webrtc_stream(websocket, camera_url)
else:
logger.error("No camera URL provided.  Cannot start stream.")
elif message["command"] == "Ice_Candidate":  # Handle ICE candidates
logger.info("Received ICE candidate from client")
if self.pc is not None:  # Ensure pc is initialized
# Extract the full candidate object from the received message
candidate_data = message.get("candidate")
logger.info(candidate_data)
ice_candidate = RTCIceCandidate(
foundation=candidate_data.get('foundation'),
component=candidate_data.get('component'),
priority=candidate_data.get('priority'),
ip=candidate_data.get('address'),
port=candidate_data.get('port'),
type=candidate_data.get('type'),
protocol=candidate_data.get('protocol'),
sdpMid=candidate_data.get('sdpMid'),
sdpMLineIndex=candidate_data.get('sdpMLineIndex')
)

# Add the ICE candidate to the peer connection
await self.pc.addIceCandidate(ice_candidate)
logger.info(f"ICE candidate added: {ice_candidate}")
else:
logger.error("Peer connection not initialized;  cannot add ICE candidate.")

except WebSocketDisconnect as e:
logger.info(f"WebSocket disconnected: {e}")
break

async def start_webrtc_stream(self, websocket, camera_url):
logger.info("Initializing WebRTC stream")

# Receive offer from client
offer = await websocket.receive_text()
logger.info(f"Raw offer received from client: {offer}")

# Parse the offer
offer = json.loads(offer)

# Proceed with WebRTC setup
self.pc = RTCPeerConnection()

@self.pc.on("icegatheringstatechange")
async def on_icegatheringstatechange():
logger.info(f"ICE gathering state: {self.pc.iceGatheringState}")

@self.pc.on("iceconnectionstatechange")
async def on_iceconnectionstatechange():
logger.info(f"ICE connection state: {self.pc.iceConnectionState}")

# Set up video track using the camera URL
player = SyntheticVideoTrack(fps=15, width=1280, height=720, color=(0, 0, 255))  # Blue frames
logger.info(f"Adding camera video track to peer connection: {camera_url}")
self.pc.addTrack(player)

# Set remote description using the client's SDP offer
offer_sdp = RTCSessionDescription(sdp=offer["sdp"], type=offer["type"])
await self.pc.setRemoteDescription(offer_sdp)

# Create answer and set local description
answer = await self.pc.createAnswer()
await self.pc.setLocalDescription(answer)
logger.info(f"SDP Answer from server:\n{self.pc.localDescription.sdp}")

# Send SDP answer back to client
await websocket.send_text(json.dumps({
"sdp": self.pc.localDescription.sdp,
"type": self.pc.localDescription.type
}))
logger.info("SDP answer sent to client successfully.")

async def shutdown_webrtc(self):
if self.pc:
await self.pc.close()
self.pc = None
logger.info("WebRTC connection closed")

# FastAPI router setup
ip_camera_route = APIRouter()
processor = VideoProcessor()

@ip_camera_route.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await asyncio.create_task(processor.websocket_control(websocket))
Сведения о проблеме
  • Сервер получает предложение SDP клиента и кандидатов ICE, отвечает ответом SDP и кандидатами ICE .
  • Клиент обрабатывает ответ SDP и показывает, что состояние соединения ICE меняется на подключенное.
  • Вскоре после этого состояние соединения клиента меняется на закрытое и затем произошел сбой.
  • На стороне сервера он регистрирует «Согласие на отправку истекло» после отображения состояния завершения соединения ICE.
Воспроизведенные выходные данные и журналы

Код: Выделить всё

Server Logs:

Код: Выделить всё

INFO:     Adding camera video track to peer connection: http://129.125.136.20/axis-cgi/mjpg/video.cgi?camera=1
INFO:     ICE gathering state: gathering
INFO:     ICE gathering state: complete
INFO:     SDP Answer from server:
v=0
o=- 3938952277 3938952277 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic:WMS *
m=video 58160 UDP/TLS/RTP/SAVPF 96 100
c=IN IP4 172.18.0.2
a=sendrecv
a=mid:0
a=msid:20030b46-dcd7-402e-91a9-61acc96fc861 0526e15c-fb87-49ec-a372-9d752dbe5f46
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=ssrc:2884284437 cname:f6435a48-f40f-4fbc-8ac2-5833d73158f4
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtpmap:100 H264/90000
a=rtcp-fb:100 goog-remb
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=candidate:2042d42f166ed704ca002a0751b390c3 1 udp 2130706431 172.18.0.2 58160 typ host
a=candidate:b0d9b1b22e9ac473e83e963403e57c55 1 udp 1694498815 37.228.203.142 27206 typ srflx raddr 172.18.0.2 rport 58160
a=end-of-candidates
a=ice-ufrag:9GzX
a=ice-pwd:6ErYyxnwubAqbQOsEEf8np
a=fingerprint:sha-256 D6:D6:BE:00:51:1E:B4:DB:90:4C:EC:63:BC:00:E0:27:3E:97:1F:A2:61:10:8D:AB:5C:7D:CE:4D:8A:5A:03:79
a=setup:active
INFO:     SDP answer sent to client successfully.
INFO:     Received message: {'command': 'Ice_Candidate', 'candidate': {'IceServer': None, 'candidate': '3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0', 'sdpMid':  None, 'sdpMLineIndex': 0, 'foundation': '3168018052', 'component': 1, 'priority': 2113937663, 'address': '192.168.0.82', 'protocol': 0, 'port': 53352, 'type': 0, 'tcpType': 0, 'relatedAddress': None, 'relatedPort': 0, 'usernameFragment': 'RRVM', 'DestinationEndPoint': None}}
INFO:     Received ICE candidate from client
INFO:     {'IceServer': None, 'candidate': '3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0', 'sdpMid': None, 'sdpMLineIndex': 0, 'foundation': '3168018052', 'component': 1, 'priority': 2113937663, 'address': '192.168.0.82', 'protocol': 0, 'port': 53352, 'type': 0, 'tcpType': 0, 'relatedAddress': None, 'relatedPort': 0, 'usernameFragment': 'RRVM', 'DestinationEndPoint': None}
INFO:     ICE candidate added: RTCIceCandidate(component=1, foundation='3168018052', ip='192.168.0.82', port=53352, priority=2113937663, protocol=0, type=0, relatedAddress=None, relatedPort=None, sdpMid=None, sdpMLineIndex=0, tcpType=None)
INFO:     Connection(3) Check CandidatePair(('172.18.0.2', 58160) -> ('192.168.0.82', 53352)) State.FROZEN -> State.WAITING
INFO:     ICE connection state: checking
INFO:     Connection(3) Check CandidatePair(('172.18.0.2', 58160) -> ('192.168.0.82', 53352)) State.WAITING -> State.IN_PROGRESS
INFO:     Connection(3) Check CandidatePair(('172.18.0.2', 58160) -> ('192.168.0.82', 53352)) State.IN_PROGRESS ->  State.SUCCEEDED
INFO:     Connection(3) ICE completed
INFO:     ICE connection state: completed
INFO:     Connection(3) Consent to send expired
INFO:     ICE connection state: failed

Код: Выделить всё

Client Logs:

Код: Выделить всё

Video track added to PeerConnection.
Creating SDP offer.
SDP Offer created:
v=0
o=- 42541 0 IN IP4 127.0.0.1
s=sipsorcery
t=0 0
a=group:BUNDLE 0
m=video 9 UDP/TLS/RTP/SAVP 96 100
c=IN IP4 0.0.0.0
a=ice-ufrag:RRVM
a=ice-pwd:LJSKIMKDKLTSBQVKWDUMSJFN
a=fingerprint:sha-256 1A:78:0D:CD:C2:A3:9F:C0:75:0D:87:D4:F9:09:07:66:91:85:E9:C5:06:AB:F1:2F:39:78:F1:DD:BD:6D:75:24
a=setup:actpass
a=candidate:3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0
a=ice-options:ice2,trickle
a=mid:0
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtpmap:100 H264/90000
a=rtcp-fb:100 goog-remb
a=fmtp:100 packetization-mode=1
a=rtcp-mux
a=rtcp:9 IN IP4 0.0.0.0
a=end-of-candidates
a=sendrecv
a=ssrc:449817168 cname:841fc310-157d-4732-9ab4-39f2f6249f20

Message sent: {"sdp":"v=0\r\no=- 42541 0 IN IP4 127.0.0.1\r\ns=sipsorcery\r\nt=0 0\r\na=group:BUNDLE 0\r\nm=video 9 UDP/TLS/RTP/SAVP 96 100\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:RRVM\r\na=ice-pwd:LJSKIMKDKLTSBQVKWDUMSJFN\r\na=fingerprint:sha-256 1A:78:0D:CD:C2:A3:9F:C0:75:0D:87:D4:F9:09:07:66:91:85:E9:C5:06:AB:F1:2F:39:78:F1:DD:BD:6D:75:24\r\na=setup:actpass\r\na=candidate:3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0\r\na=ice-options:ice2,trickle\r\na=mid:0\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtpmap:100 H264/90000\r\na=rtcp-fb:100 goog-remb\r\na=fmtp:100 packetization-mode=1\r\na=rtcp-mux\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=end-of-candidates\r\na=sendrecv\r\na=ssrc:449817168 cname:841fc310-157d-4732-9ab4-39f2f6249f20\r\n","type":"offer"}
SDP Offer sent to server.
ICE candidate gathered:
3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0
Message sent: {"command":"Ice_Candidate","candidate":{"IceServer":null,"candidate":"3168018052 1 udp 2113937663 192.168.0.82 53352 typ host generation 0","sdpMid":null,"sdpMLineIndex":0,"foundation":"3168018052","component":1,"priority":2113937663,"address":"192.168.0.82","protocol":0,"port":53352,"type":0,"tcpType":0,"relatedAddress":null,"relatedPort":0,"usernameFragment":"RRVM","DestinationEndPoint":null}}
ICE candidate sent to server.
WebRTC connection setup complete.
Received message from server: {"sdp": "v=0\r\no=- 3938952277 3938952277 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=video 58160 UDP/TLS/RTP/SAVPF 96 100\r\nc=IN IP4 172.18.0.2\r\na=sendrecv\r\na=mid:0\r\na=msid:20030b46-dcd7-402e-91a9-61acc96fc861 0526e15c-fb87-49ec-a372-9d752dbe5f46\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=rtcp-mux\r\na=ssrc:2884284437 cname:f6435a48-f40f-4fbc-8ac2-5833d73158f4\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtpmap:100 H264/90000\r\na=rtcp-fb:100 goog-remb\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=candidate:2042d42f166ed704ca002a0751b390c3 1 udp 2130706431 172.18.0.2 58160 typ host\r\na=candidate:b0d9b1b22e9ac473e83e963403e57c55 1 udp 1694498815 37.228.203.142 27206 typ srflx raddr 172.18.0.2 rport 58160\r\na=end-of-candidates\r\na=ice-ufrag:9GzX\r\na=ice-pwd:6ErYyxnwubAqbQOsEEf8np\r\na=fingerprint:sha-256 D6:D6:BE:00:51:1E:B4:DB:90:4C:EC:63:BC:00:E0:27:3E:97:1F:A2:61:10:8D:AB:5C:7D:CE:4D:8A:5A:03:79\r\na=setup:active\r\n", "type":  "answer"}
Received SDP Answer:
v=0
o=- 3938952277 3938952277 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic:WMS *
m=video 58160 UDP/TLS/RTP/SAVPF 96 100
c=IN IP4 172.18.0.2
a=sendrecv
a=mid:0
a=msid:20030b46-dcd7-402e-91a9-61acc96fc861 0526e15c-fb87-49ec-a372-9d752dbe5f46
a=rtcp:9 IN IP4 0.0.0.0
a=rtcp-mux
a=ssrc:2884284437 cname:f6435a48-f40f-4fbc-8ac2-5833d73158f4
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtpmap:100 H264/90000
a=rtcp-fb:100 goog-remb
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=candidate:2042d42f166ed704ca002a0751b390c3 1 udp 2130706431 172.18.0.2 58160 typ host
a=candidate:b0d9b1b22e9ac473e83e963403e57c55 1 udp 1694498815 37.228.203.142 27206 typ srflx raddr 172.18.0.2 rport 58160
a=end-of-candidates
a=ice-ufrag:9GzX
a=ice-pwd:6ErYyxnwubAqbQOsEEf8np
a=fingerprint:sha-256 D6:D6:BE:00:51:1E:B4:DB:90:4C:EC:63:BC:00:E0:27:3E:97:1F:A2:61:10:8D:AB:5C:7D:CE:4D:8A:5A:03:79
a=setup:active

ICE connection state changed to: checking
SDP Answer set on PeerConnection.
ICE connection state changed to: connected
Connection state changed to connecting
Connection state changed to closed
Connection state changed to failed
  • Кандидаты ICE и обмен SDP кажутся успешными. Однако состояние соединения клиента меняется на «Не удалось» вскоре после переключения на «Подключено».
    Сервер регистрирует «Согласие на отправку истекло» сразу после сообщения о состоянии соединения ICE: завершено..
Есть ли какой-то шаг, который нам не хватает для поддержания соединения, или проблема с конфигурацией ICE? Будем очень признательны за любую информацию!

Подробнее здесь: https://stackoverflow.com/questions/791 ... nd-expired
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «Python»