Skip to content

Commit 16a13e9

Browse files
FaranMustafaSean-Der
authored andcommitted
Add gocv-to-webrtc
Add an example of using GoCV that sends the video back to the client via WebRTC. The existing example gocv-receive decodes and plays on the server.
1 parent 3a8f018 commit 16a13e9

File tree

3 files changed

+443
-0
lines changed

3 files changed

+443
-0
lines changed

gocv-to-webrtc/README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# GoCV + FFmpeg + Pion WebRTC
2+
3+
This project demonstrates how to stream live webcam video to a browser using GoCV for camera capture, FFmpeg for real‑time VP8 encoding, and Pion WebRTC for media transport.
4+
5+
---
6+
7+
## Overview
8+
9+
- **Capture**: Uses [GoCV](https://gocv.io/) to access a webcam and read raw BGR frames.
10+
- **Encode**: Pipes raw frames into `ffmpeg` for VP8 encoding in IVF format.
11+
- **Stream**: Uses [Pion WebRTC](https://github.com/pion/webrtc) to send encoded video frames to a browser client.
12+
- **Frontend**: Minimal HTML/JS page that negotiates WebRTC Offer/Answer and displays incoming video.
13+
14+
---
15+
16+
## Prerequisites
17+
18+
- Go 1.20 or newer
19+
- FFmpeg installed with `libvpx` support
20+
- OpenCV 4.x installed
21+
- GoCV installed (`go get -u -d gocv.io/x/gocv`)
22+
- A working webcam (USB or internal)
23+
24+
---
25+
26+
## Download & Install the Example
27+
28+
Install and run the `gocv-to-webrtc` example directly:
29+
30+
```bash
31+
go install github.com/pion/webrtc/v4/examples/govc-to-webrtc@latest
32+
```
33+
34+
On macOS, set the camera index to `0` (instead of `2`) by editing `main.go`:
35+
36+
```go
37+
webcam, _ := gocv.OpenVideoCapture(0)
38+
```
39+
40+
Then run:
41+
42+
```bash
43+
gocv-to-webrtc
44+
```
45+
46+
---
47+
48+
## Usage
49+
50+
1. Run the example:
51+
```bash
52+
gocv-to-webrtc
53+
```
54+
2. Open your browser at `http://localhost:8080`.
55+
3. Click **Start Session** to initiate WebRTC negotiation.
56+
4. After ICE connects, you should see your webcam video in the page.
57+
58+
---
59+
60+
## How It Works
61+
62+
### Server (`main.go`)
63+
64+
1. **HTTP Server**
65+
- Serves static files (`static/`) on port 8080.
66+
- Handles `/offer` endpoint for SDP exchange.
67+
68+
2. **WebRTC Setup**
69+
- Reads the browser’s SDP Offer.
70+
- Creates a Pion `PeerConnection` with a VP8 track (`TrackLocalStaticSample`).
71+
- Sets remote description, creates Answer, and returns it once ICE gathering completes.
72+
- Starts the camera stream after ICE connection.
73+
74+
3. **Video Pipeline** (`startCameraAndStream`)
75+
- Opens webcam via GoCV (`gocv.OpenVideoCapture`).
76+
- Pipes raw BGR frames into FFmpeg:
77+
```bash
78+
ffmpeg -y \
79+
-f rawvideo -pixel_format bgr24 -video_size 640x480 -framerate 30 -i pipe:0 \
80+
-c:v libvpx -b:v 1M -f ivf pipe:1
81+
```
82+
- Reads VP8 IVF frames from FFmpeg’s stdout with `ivfreader`.
83+
- Writes frames into the WebRTC track.
84+
85+
### Frontend (`static/index.html`)
86+
87+
1. Creates an `RTCPeerConnection` with STUN.
88+
2. Adds a `recvonly` video transceiver.
89+
3. Sends SDP Offer to server.
90+
4. Sets remote Answer.
91+
5. Attaches incoming stream to a `<video>` element.
92+
93+
---
94+
95+
## Configuration
96+
97+
- **Camera Device**: Change `gocv.OpenVideoCapture(2)` to the appropriate index (e.g., `0`).
98+
- **Resolution & Frame Rate**: Adjust GoCV settings and FFmpeg flags (`-video_size`, `-framerate`).
99+
- **Frame Rate**: Adjust `-framerate 30` in FFmpeg and ticker interval in Go.
100+
- **Bitrate & Codec**: Modify `-b:v 1M` or swap codecs (H264/Opus).
101+
102+
---
103+
104+
## Troubleshooting
105+
106+
- **No Video**: Check camera index and FFmpeg installation.
107+
- **ICE Fails**: Verify STUN server and network/firewall.
108+
- **High CPU**: Lower resolution/bitrate or tune FFmpeg CPU usage.
109+
110+
---
111+
112+
## Related Libraries
113+
114+
- [GoCV](https://gocv.io/)
115+
- [FFmpeg](https://ffmpeg.org/)
116+
- [Pion WebRTC](https://github.com/pion/webrtc)
117+
- [ivfreader](https://pkg.go.dev/github.com/pion/webrtc/v4/pkg/media/ivfreader)
118+
119+
---
120+
121+
## License
122+
123+
MIT License.
124+
125+
---
126+
127+
## Acknowledgements
128+
129+
Inspired by [pion/webrtc examples](https://github.com/pion/webrtc/tree/master/examples).
130+

gocv-to-webrtc/main.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"log"
10+
"net/http"
11+
"os/exec"
12+
"time"
13+
14+
"github.com/pion/webrtc/v4"
15+
"github.com/pion/webrtc/v4/pkg/media"
16+
"github.com/pion/webrtc/v4/pkg/media/ivfreader"
17+
gocv "gocv.io/x/gocv"
18+
)
19+
20+
func main() {
21+
// Serve the static/ folder on http://localhost:8080
22+
http.Handle("/", http.FileServer(http.Dir("static")))
23+
24+
// POST /offer will handle the browser's WebRTC offer
25+
http.HandleFunc("/offer", handleOffer)
26+
27+
fmt.Println("Listening on http://localhost:8080")
28+
log.Fatal(http.ListenAndServe(":8080", nil))
29+
}
30+
31+
func handleOffer(w http.ResponseWriter, r *http.Request) {
32+
// 1) Read the Offer from the browser
33+
var offer webrtc.SessionDescription
34+
if err := json.NewDecoder(r.Body).Decode(&offer); err != nil {
35+
http.Error(w, "invalid offer", http.StatusBadRequest)
36+
return
37+
}
38+
39+
// 2) Create a new PeerConnection
40+
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
41+
ICEServers: []webrtc.ICEServer{
42+
{URLs: []string{"stun:stun.l.google.com:19302"}},
43+
},
44+
})
45+
if err != nil {
46+
http.Error(w, "failed to create PeerConnection", http.StatusInternalServerError)
47+
return
48+
}
49+
50+
// 3) Create a video track for VP8
51+
videoTrack, err := webrtc.NewTrackLocalStaticSample(
52+
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8},
53+
"video", // track id
54+
"gocv", // stream id
55+
)
56+
if err != nil {
57+
http.Error(w, "failed to create video track", http.StatusInternalServerError)
58+
return
59+
}
60+
61+
// Add the track to the PeerConnection
62+
rtpSender, err := peerConnection.AddTrack(videoTrack)
63+
if err != nil {
64+
http.Error(w, "failed to add track", http.StatusInternalServerError)
65+
return
66+
}
67+
68+
// Read RTCP (for NACK, etc.) in a separate goroutine
69+
go func() {
70+
rtcpBuf := make([]byte, 1500)
71+
for {
72+
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
73+
return
74+
}
75+
}
76+
}()
77+
78+
// 4) Watch for ICE connection state
79+
iceConnectedCtx, iceConnectedCancel := context.WithCancel(context.Background())
80+
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
81+
log.Printf("ICE state: %s\n", state)
82+
if state == webrtc.ICEConnectionStateConnected {
83+
iceConnectedCancel()
84+
}
85+
})
86+
87+
// 5) Set the remote description (the browser's Offer)
88+
if err := peerConnection.SetRemoteDescription(offer); err != nil {
89+
http.Error(w, "failed to set remote desc", http.StatusInternalServerError)
90+
return
91+
}
92+
93+
// 6) Create an Answer
94+
answer, err := peerConnection.CreateAnswer(nil)
95+
if err != nil {
96+
http.Error(w, "failed to create answer", http.StatusInternalServerError)
97+
return
98+
}
99+
100+
// 7) Gather ICE candidates
101+
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
102+
if err := peerConnection.SetLocalDescription(answer); err != nil {
103+
http.Error(w, "failed to set local desc", http.StatusInternalServerError)
104+
return
105+
}
106+
<-gatherComplete
107+
108+
// 8) Write the Answer back to the browser
109+
w.Header().Set("Content-Type", "application/json")
110+
_ = json.NewEncoder(w).Encode(peerConnection.LocalDescription())
111+
112+
// 9) Once ICE is connected, start reading frames from the camera via GoCV,
113+
// pipe them into FFmpeg for VP8 encoding, and push the IVF frames into the track.
114+
go func() {
115+
<-iceConnectedCtx.Done()
116+
117+
if err := startCameraAndStream(videoTrack); err != nil {
118+
log.Printf("camera streaming error: %v\n", err)
119+
}
120+
}()
121+
}
122+
123+
// startCameraAndStream opens the webcam with GoCV, sends raw frames to FFmpeg (via stdin),
124+
// reads IVF from FFmpeg (via stdout), and writes them into the WebRTC video track.
125+
func startCameraAndStream(videoTrack *webrtc.TrackLocalStaticSample) error {
126+
// Open default camera with GoCV
127+
webcam, err := gocv.OpenVideoCapture(2)
128+
if err != nil {
129+
return fmt.Errorf("cannot open camera: %w", err)
130+
}
131+
defer webcam.Close()
132+
133+
// Set some camera settings if needed
134+
// e.g. webcam.Set(gocv.VideoCaptureFrameWidth, 640)
135+
// webcam.Set(gocv.VideoCaptureFrameHeight, 480)
136+
// Or rely on defaults
137+
138+
// Prepare FFmpeg cmd:
139+
// -f rawvideo: We feed raw frames
140+
// -pixel_format bgr24: Our GoCV frames come in BGR format
141+
// -video_size 640x480: must match your actual capture size
142+
// -i pipe:0 : read from stdin
143+
// Then encode with libvpx -> IVF on stdout
144+
ffmpeg := exec.Command(
145+
"ffmpeg",
146+
"-y",
147+
"-f", "rawvideo",
148+
"-pixel_format", "bgr24",
149+
"-video_size", "640x480",
150+
"-framerate", "30", // assume ~30fps
151+
"-i", "pipe:0",
152+
"-c:v", "libvpx",
153+
"-b:v", "1M",
154+
"-f", "ivf",
155+
"pipe:1",
156+
)
157+
158+
stdin, err := ffmpeg.StdinPipe()
159+
if err != nil {
160+
return fmt.Errorf("ffmpeg stdin error: %w", err)
161+
}
162+
stdout, err := ffmpeg.StdoutPipe()
163+
if err != nil {
164+
return fmt.Errorf("ffmpeg stdout error: %w", err)
165+
}
166+
167+
// Start FFmpeg
168+
if err := ffmpeg.Start(); err != nil {
169+
return fmt.Errorf("failed to start ffmpeg: %w", err)
170+
}
171+
172+
// Goroutine to write raw frames to FFmpeg stdin
173+
go func() {
174+
defer stdin.Close()
175+
176+
frame := gocv.NewMat()
177+
defer frame.Close()
178+
179+
ticker := time.NewTicker(time.Millisecond * 33) // ~30fps
180+
defer ticker.Stop()
181+
182+
for range ticker.C {
183+
if ok := webcam.Read(&frame); !ok {
184+
log.Println("cannot read frame from camera")
185+
continue
186+
}
187+
if frame.Empty() {
188+
continue
189+
}
190+
191+
// (Optional) do any OpenCV processing on `frame` here
192+
193+
// Write raw BGR bytes to FFmpeg
194+
// frame.DataPtrUint8() points to the underlying byte array
195+
_, _ = stdin.Write(frame.ToBytes())
196+
}
197+
}()
198+
199+
// Read IVF from FFmpeg stdout; parse frames with ivfreader
200+
ivf, _, err := ivfreader.NewWith(stdout)
201+
if err != nil {
202+
return fmt.Errorf("ivfreader init error: %w", err)
203+
}
204+
// Loop reading IVF frames; push them to the video track
205+
for {
206+
frame, _, err := ivf.ParseNextFrame()
207+
if errors.Is(err, io.EOF) {
208+
log.Println("ffmpeg ended (EOF)")
209+
break
210+
}
211+
if err != nil {
212+
return fmt.Errorf("ivf parse error: %w", err)
213+
}
214+
// Deliver the VP8 frame
215+
writeErr := videoTrack.WriteSample(media.Sample{
216+
Data: frame,
217+
Duration: time.Second / 30,
218+
})
219+
if writeErr != nil {
220+
return fmt.Errorf("write sample error: %w", writeErr)
221+
}
222+
}
223+
224+
// Wait for ffmpeg to exit
225+
if err := ffmpeg.Wait(); err != nil {
226+
return fmt.Errorf("ffmpeg wait error: %w", err)
227+
}
228+
229+
return nil
230+
}

0 commit comments

Comments
 (0)