Implement WebRTC offer handling and ICE candidate management in voice_webrtc.py
- Add new HTTP endpoints for handling WebRTC offers and ICE candidates, enhancing the signaling process for voice interactions. - Introduce dynamic variable decoding from request headers to support flexible offer payloads. - Refactor existing WebSocket handling to accommodate new offer processing logic. - Update frontend dependencies to include Pipecat client libraries for improved WebRTC transport management. - Streamline voice preview functionality by integrating SmallWebRTCTransport for better media handling.
This commit is contained in:
@@ -9,9 +9,11 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
|
||||
from db.session import SessionLocal
|
||||
from fastapi import APIRouter, Depends, WebSocket
|
||||
from fastapi import APIRouter, Body, Depends, Request, WebSocket
|
||||
from loguru import logger
|
||||
from models import AssistantConfig, SignalingOffer
|
||||
from services.auth import require_admin, require_admin_websocket
|
||||
@@ -24,6 +26,7 @@ from services.webrtc_ice import aiortc_ice_servers, client_ice_servers
|
||||
# 注意:pipecat 是重依赖(语音才用),在 _handle_offer 等处惰性导入。
|
||||
|
||||
router = APIRouter(tags=["voice"])
|
||||
_http_peers: dict[str, object] = {}
|
||||
|
||||
|
||||
@router.get("/api/webrtc/ice-servers", dependencies=[Depends(require_admin)])
|
||||
@@ -32,6 +35,45 @@ async def ice_servers():
|
||||
return {"iceServers": client_ice_servers()}
|
||||
|
||||
|
||||
@router.post("/api/webrtc/offer", dependencies=[Depends(require_admin)])
|
||||
async def http_offer(request: Request, payload: dict = Body(...)):
|
||||
"""Official SmallWebRTC JS transport offer endpoint."""
|
||||
request_data = payload.get("requestData") or {}
|
||||
encoded_variables = request.headers.get("x-pipecat-dynamic-variables", "")
|
||||
header_variables = {}
|
||||
if encoded_variables:
|
||||
padding = "=" * (-len(encoded_variables) % 4)
|
||||
header_variables = json.loads(
|
||||
base64.urlsafe_b64decode(encoded_variables + padding).decode("utf-8")
|
||||
)
|
||||
offer_payload = {
|
||||
**payload,
|
||||
"assistant_id": request.headers.get("x-pipecat-assistant-id"),
|
||||
"dynamic_variables": header_variables,
|
||||
**request_data,
|
||||
}
|
||||
answer = await _handle_offer_payload(offer_payload, _http_peers)
|
||||
return answer
|
||||
|
||||
|
||||
@router.patch("/api/webrtc/offer", dependencies=[Depends(require_admin)])
|
||||
async def http_ice_candidates(payload: dict = Body(...)):
|
||||
"""Accept the batched trickle ICE format used by the official JS client."""
|
||||
pc_id = payload.get("pc_id")
|
||||
pc = _http_peers.get(pc_id) if pc_id else None
|
||||
if not pc:
|
||||
return {"ok": False}
|
||||
|
||||
from aiortc.sdp import candidate_from_sdp
|
||||
|
||||
for item in payload.get("candidates") or []:
|
||||
candidate = candidate_from_sdp(item.get("candidate", ""))
|
||||
candidate.sdpMid = item.get("sdp_mid")
|
||||
candidate.sdpMLineIndex = item.get("sdp_mline_index")
|
||||
await pc.add_ice_candidate(candidate)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.websocket("/ws/voice")
|
||||
async def voice_signaling(websocket: WebSocket):
|
||||
if not await require_admin_websocket(websocket):
|
||||
@@ -92,6 +134,17 @@ async def _resolve_config(offer: SignalingOffer) -> AssistantConfig:
|
||||
|
||||
|
||||
async def _handle_offer(websocket, payload, peers):
|
||||
answer = await _handle_offer_payload(payload, peers)
|
||||
if websocket.application_state == WebSocketState.CONNECTED:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "answer",
|
||||
"payload": answer,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _handle_offer_payload(payload, peers):
|
||||
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
|
||||
from services.pipecat.pipeline import run_pipeline
|
||||
from services.pipecat.transports import build_webrtc_transport
|
||||
@@ -99,6 +152,11 @@ async def _handle_offer(websocket, payload, peers):
|
||||
offer = SignalingOffer(**payload)
|
||||
pc_id = offer.pc_id
|
||||
|
||||
restart_pc = bool(payload.get("restart_pc"))
|
||||
if restart_pc and pc_id and pc_id in peers:
|
||||
old_pc = peers.pop(pc_id)
|
||||
await old_pc.disconnect()
|
||||
|
||||
if pc_id and pc_id in peers:
|
||||
pc = peers[pc_id]
|
||||
await pc.renegotiate(sdp=offer.sdp, type=offer.type, restart_pc=False)
|
||||
@@ -144,17 +202,11 @@ async def _handle_offer(websocket, payload, peers):
|
||||
)
|
||||
|
||||
answer = pc.get_answer()
|
||||
if websocket.application_state == WebSocketState.CONNECTED:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "answer",
|
||||
"payload": {
|
||||
"pc_id": answer["pc_id"],
|
||||
"sdp": answer["sdp"],
|
||||
"type": answer["type"],
|
||||
},
|
||||
}
|
||||
)
|
||||
return {
|
||||
"pc_id": answer["pc_id"],
|
||||
"sdp": answer["sdp"],
|
||||
"type": answer["type"],
|
||||
}
|
||||
|
||||
|
||||
async def _handle_ice(payload, peers):
|
||||
|
||||
Reference in New Issue
Block a user