EpicGamesExt / PixelStreamingInfrastructure

The official Pixel Streaming servers and frontend.
MIT License
200 stars 75 forks source link

Python video capture #257

Closed reza-shahriari closed 1 week ago

reza-shahriari commented 2 weeks ago

Component your question relates to

Question

Hello everyone. Nice repo thanks for it 🙏 I found it really useful and it solved my biggest problem 😁 Is there any way I can capture the video from python outside unreal? Everything works good and I can open web browsers and see the streamed results but when I want to read video from the URL (localhost/?streamid=camera_1) there is no data in it. I have tested other URLs as well but I could not figure out how to read images Would you help me please Thanks a lot

reza-shahriari commented 2 weeks ago

i have found this code useful:

import asyncio
import cv2
import numpy as np
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, VideoStreamTrack
from aiortc.contrib.media import MediaRelay
import websockets
import json
import av
import time

class PixelStreamerVideoTrack(VideoStreamTrack):
    def __init__(self, fps):
        super().__init__()
        self.frame_count = 0
        self.fps = fps
        self.pts_increment = 1000 // fps  # Time increment per frame in milliseconds

    async def recv(self):
        self.frame_count += 1

        # Calculate the pts based on the frame count and FPS
        pts = self.frame_count * self.pts_increment

        height, width = 1080, 1920
        frame = np.zeros((height, width, 3), dtype=np.uint8)

        # Convert the frame to an av.VideoFrame
        video_frame = av.VideoFrame.from_ndarray(frame, format="bgr24")
        video_frame.pts = pts
        video_frame.time_base = av.Rational(1, 1000)  # time_base in milliseconds

        return video_frame

def modify_sdp_with_stream_id(sdp, stream_id):
    lines = sdp.splitlines()
    inserted = False

    for i, line in enumerate(lines):
        if line.startswith("m="):  # Media section
            lines.insert(i + 1, f"a=msid-semantic: WMS {stream_id}")
            inserted = True
            break

    if not inserted:
        # Handle error: stream ID not inserted
        pass

    return "\n".join(lines)

async def connect_to_pixel_streamer(streamer_id,fps):
    pc = RTCPeerConnection()
    relay = MediaRelay()

    @pc.on("track")
    def on_track(track):
        print(f"Receiving {track.kind} track")
        if track.kind == "video":
            local_video = relay.subscribe(track)

            async def consume_video():
                frame_count  = 0 
                t0 = time.time()
                while True:
                    try:
                        frame_count+=1
                        frame = await local_video.recv()
                        # print("video_frame is : ", frame)
                        # print("Received frame resolution:", frame.width, "x", frame.height)
                        # print("Avg Frame Rate: ", frame_count/(time.time() - t0))
                        # img = cv2.cvtColor(np.array(frame.to_ndarray(format="bgr24")), cv2.COLOR_RGB2BGR)
                        img = np.array(frame.to_ndarray(format="bgr24"))
                        cv2.imshow('Pixel Streamer Video', img)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            break
                    except Exception as e:
                        print(f"Error processing video frame: {e}")
                        break

            asyncio.ensure_future(consume_video())

    # Add a video sender to create an offer with media
    pc.addTrack(PixelStreamerVideoTrack(fps=fps,))

    # Connect to the signaling server
    async with websockets.connect(f"ws://localhost/") as ws:
        # Send offer
        offer = await pc.createOffer()
        print(offer)
        print("****************************************************************")
        offer.sdp = modify_sdp_with_stream_id(offer.sdp, streamer_id)
        print(offer)
        await pc.setLocalDescription(offer)
        message = json.dumps({
        "type": "offer",
        "sdp": pc.localDescription.sdp,
    })
        await ws.send(message)

        print("Offer sent, waiting for answer...")

        while True:
            response = await ws.recv()
            # print(f"Received message: {response}")
            data = json.loads(response)
            # for k , v in data.items():
            #     print(f"key: {k}, value: {v}")
            if data["type"] == "answer":
                print("Received answer")
                await pc.setRemoteDescription(RTCSessionDescription(sdp=data["sdp"], type="answer"))
            elif data["type"] == "iceCandidate":
                print("Received ICE candidate")
                candidate = data["candidate"]["candidate"]
                try:
                    await pc.addIceCandidate(candidate)
                except Exception as e:
                    print(f"Error adding ICE candidate: {e}")
            elif data["type"] == "config":
                print("Received config")
                # Handle config if needed
            elif data["type"] == "playerCount":
                print(f"Player count: {data['count']}")
            else:
                print(f"Unhandled message type: {data['type']}")

            # Check if connection is established
            if pc.iceConnectionState == "connected":
                print("WebRTC connection established")
                break

        # Keep the connection alive
        while True:
            try:
                message = await asyncio.wait_for(ws.recv(), timeout=1.0)
                print(f"Received message: {message}")
            except asyncio.TimeoutError:
                pass  # No message received, continue
            except websockets.exceptions.ConnectionClosed:
                print("WebSocket connection closed")
                break

if __name__ == "__main__":
    # streamer_id = input("Enter the StreamerId: ")
    streamer_id = "Camera1"
    fps = 60
    asyncio.run(connect_to_pixel_streamer(streamer_id,fps))

I'm experiencing low image quality when streaming video at 1920x1080 resolution and reading in the same size. The image quality is lower than expected. This issue only occurs when using a python, as I don't encounter the problem when using Chrome. Additionally, I'm unable to access my different cameras. No matter what streamer_id I input, I only get results from camera2. I've tried changing ws://localhost/ to ws://localhost/?streamerId=Camera1, but it doesn't solve the problem.

mcottontensor commented 2 weeks ago

Python is not something we support currently and so cannot really provide any input for this. However it should be reasonably straight forward to provide a video stream by following the UE plugin as a reference.

Additionally I currently have a JavaScript implementation of a streamer that might be helpful for you at my fork. This is something that is being developed to help testing and has very limited functionality but should provide a good reference for the bare minimum.

It sounds like you have several issues.

If you cannot switch streamers using the streamerId parameter then it sounds like your streamers are not identifying themselves correctly. It's also possible you're confusing "stream id" with "streamer id". Each streamer connected to the signalling server has it's own unique streamer id and that is what the streamerId parameter is selecting. We currently do not have a way to select individual streams from the one streamer.

The quality of the video stream will be completely up to the encoder or the quality of the stream provided. The front end will just display whatever video stream it receives.

Hope this helps.

reza-shahriari commented 1 week ago

It helped a lot, Thanks for the answer