import asyncio import json import os import signal import websockets import gi gi.require_version("Gst", "1.0") gi.require_version("GstWebRTC", "1.0") gi.require_version("GstSdp", "1.0") from gi.repository import Gst, GstWebRTC, GstSdp SIGNALING_URL = os.environ.get("SIGNALING_URL") or os.environ.get("CAMERA_SIGNALING_URL", "ws://127.0.0.1:3000") ROOM_ID = os.environ.get("ROOM_ID", "jetson-1") VIDEO_DEVICE = os.environ.get("VIDEO_DEVICE", "/dev/video0") VIDEO_WIDTH = int(os.environ.get("VIDEO_WIDTH", "1920")) VIDEO_HEIGHT = int(os.environ.get("VIDEO_HEIGHT", "1080")) VIDEO_FRAMERATE = int(os.environ.get("VIDEO_FRAMERATE", "30")) VIDEO_BITRATE = int(os.environ.get("VIDEO_BITRATE", "4000000")) def default_pipeline_desc(): return f""" v4l2src device={VIDEO_DEVICE} ! video/x-raw,format=UYVY,width={VIDEO_WIDTH},height={VIDEO_HEIGHT},framerate={VIDEO_FRAMERATE}/1 ! nvvidconv ! video/x-raw(memory:NVMM),format=NV12 ! nvv4l2h264enc bitrate={VIDEO_BITRATE} insert-sps-pps=true iframeinterval=30 ! h264parse ! rtph264pay config-interval=1 pt=96 ! application/x-rtp,media=video,encoding-name=H264,payload=96 ! webrtcbin name=webrtc bundle-policy=max-bundle """ PIPELINE_DESC = os.environ.get("PIPELINE_DESC") or default_pipeline_desc() ws = None loop = None pipeline = None webrtc = None stopping = False def log(message): print(message, flush=True) def send_json(data): if ws and loop and not stopping: asyncio.run_coroutine_threadsafe(ws.send(json.dumps(data)), loop) def on_offer_created(promise, _, __): promise.wait() reply = promise.get_reply() offer = reply.get_value("offer") webrtc.emit("set-local-description", offer, Gst.Promise.new()) send_json({ "type": "offer", "offer": { "type": "offer", "sdp": offer.sdp.as_text(), }, }) log("offer sent") def on_negotiation_needed(element): promise = Gst.Promise.new_with_change_func(on_offer_created, None, None) element.emit("create-offer", None, promise) def request_offer(reason): if not webrtc or stopping: return log(f"creating offer: {reason}") on_negotiation_needed(webrtc) def on_ice_candidate(element, mlineindex, candidate): send_json({ "type": "candidate", "candidate": { "candidate": candidate, "sdpMLineIndex": mlineindex, }, }) async def handle_message(message): data = json.loads(message) if data["type"] == "joined" and data.get("role") == "viewer": request_offer("viewer joined") elif data["type"] == "answer": sdp = data["answer"]["sdp"] _, sdpmsg = GstSdp.SDPMessage.new() GstSdp.sdp_message_parse_buffer(bytes(sdp.encode()), sdpmsg) answer = GstWebRTC.WebRTCSessionDescription.new( GstWebRTC.WebRTCSDPType.ANSWER, sdpmsg, ) webrtc.emit("set-remote-description", answer, Gst.Promise.new()) log("answer applied") elif data["type"] == "candidate": candidate = data["candidate"] webrtc.emit( "add-ice-candidate", candidate.get("sdpMLineIndex", 0), candidate["candidate"], ) async def main(): global ws, loop, pipeline, webrtc loop = asyncio.get_running_loop() log(f"connecting signaling {SIGNALING_URL}, room={ROOM_ID}") log("pipeline:") log(PIPELINE_DESC) pipeline = Gst.parse_launch(PIPELINE_DESC) webrtc = pipeline.get_by_name("webrtc") if not webrtc: raise RuntimeError("webrtcbin named 'webrtc' was not found in pipeline.") webrtc.connect("on-negotiation-needed", on_negotiation_needed) webrtc.connect("on-ice-candidate", on_ice_candidate) async with websockets.connect(SIGNALING_URL) as websocket: ws = websocket await ws.send(json.dumps({ "type": "join", "roomId": ROOM_ID, "role": "jetson", })) pipeline.set_state(Gst.State.PLAYING) log("pipeline started") await asyncio.sleep(0.5) request_offer("pipeline started") async for message in ws: await handle_message(message) def stop_pipeline(): global stopping stopping = True if pipeline: pipeline.set_state(Gst.State.NULL) log("pipeline stopped") def handle_stop_signal(signum, frame): stop_pipeline() raise SystemExit(0) if __name__ == "__main__": Gst.init(None) signal.signal(signal.SIGTERM, handle_stop_signal) signal.signal(signal.SIGINT, handle_stop_signal) try: asyncio.run(main()) finally: stop_pipeline()