File size: 3,214 Bytes
c551a63
6f9faf9
 
379cab9
c551a63
 
6f9faf9
c551a63
7e0b27a
c551a63
 
 
 
 
 
7e0b27a
c551a63
 
 
 
 
 
 
 
ee11946
c551a63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f9faf9
 
ee11946
c551a63
ee11946
c551a63
ee11946
6f9faf9
 
7e0b27a
 
 
 
 
 
ee11946
 
7e0b27a
 
6f9faf9
7e0b27a
 
 
 
6f9faf9
 
ee11946
 
bd6580c
6f9faf9
c551a63
 
 
 
 
7e0b27a
c551a63
 
 
 
6f9faf9
 
 
7e0b27a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import asyncio
import numpy as np
import gradio as gr
import spaces
from fastrtc import WebRTC, get_turn_credentials, AsyncStreamHandler
from fastrtc.utils import wait_for_item

class FrameFlipperHandler(AsyncStreamHandler):
    """
    A persistent, reactive handler for flipping video frames.

    This class creates a long-running process for each user connection.
    - The `receive` method is called by the backend whenever a new frame arrives from the user.
    - The `emit` method is called by the backend in a loop to get processed frames to send back.
    - An internal `asyncio.Queue` connects these two methods for reactive, low-latency processing.
    """
    def __init__(self):
        super().__init__()
        self.frame_queue = asyncio.Queue()

    async def receive(self, frame: tuple[int, np.ndarray]) -> None:
        """Called for each frame received from the client's webcam."""
        # This is non-blocking and instantly puts the frame into our processing queue.
        self.frame_queue.put_nowait(frame)
    
    @spaces.GPU
    async def emit(self):
        """Called in a loop to get the next frame to send to the client."""
        # This waits for a frame to be available in the queue with a small timeout.
        frame_data = await wait_for_item(self.frame_queue, timeout=0.01)
        
        if frame_data:
            _rate, frame_array = frame_data
            if frame_array is not None:
                # Flip the frame both vertically and horizontally
                flipped_frame = np.flip(frame_array, axis=(0, 1))
                return (None, flipped_frame) # The sample rate is ignored for video

        return None

    def copy(self):
        """Creates a new instance of this handler for each new connection."""
        return FrameFlipperHandler()


# --- Gradio UI Layout ---
with gr.Blocks(theme=gr.themes.Soft(), title="FastRTC Webcam Double Flipper") as demo:
    gr.Markdown("# 🚀 FastRTC Webcam Double Flipper (Reactive)")
    gr.Markdown(
        "*This version uses a persistent `AsyncStreamHandler` for low-latency, reactive streaming, ideal for environments like ZeroGPU.*"
    )
    
    with gr.Row():
        with gr.Column():
            gr.Markdown("### 1. Your Webcam Feed (Input)")
            webcam_input = WebRTC(
                label="Webcam Input",
                modality="video", 
                mode="send",
                width=640,
                height=480,
                rtc_configuration=get_turn_credentials(),
            )
            
        with gr.Column():
            gr.Markdown("### 2. Flipped Video (Output)")
            video_output = WebRTC(
                label="Flipped Output Stream",
                modality="video", 
                mode="receive",
                width=640,
                height=480,
                rtc_configuration=get_turn_credentials(),
            )
    
    # Instantiate our custom handler
    handler = FrameFlipperHandler()

    # Pass the handler instance to the stream method
    webcam_input.stream(
        fn=handler, 
        inputs=[webcam_input],
        outputs=[video_output],
        time_limit=120,
    )

if __name__ == "__main__":
    demo.queue().launch()