Skip to content

Commit e32fab0

Browse files
committed
Add example of using FFmpeg to send H264
1 parent 354b3b4 commit e32fab0

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

.github/.ci.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ function _install_gstreamer_hook(){
1111
sudo apt-get update
1212
sudo apt-get purge -y libunwind-14-dev
1313
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
14-
14+
sudo apt-get install -y libavcodec-dev libavutil-dev libavfilter-dev libavscale-dev libavformat-dev
1515
}

ffmpeg-send/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ffmpeg-send
2+
ffmpeg-send is a simple application that shows how to send video to your browser using Pion WebRTC and FFmpeg.
3+
4+
## Instructions
5+
### Install FFmpeg
6+
This example requires you have FFmpeg installed. See the docs of [go-astiav](https://github.com/asticode/go-astiav) for
7+
how to do this.
8+
9+
### Download ffmpeg-send
10+
```
11+
export GO111MODULE=on
12+
go install github.com/pion/example-webrtc-applications/v3/ffmpeg-send@latest
13+
```
14+
15+
### Open ffmpeg-send example page
16+
[jsfiddle.net](https://jsfiddle.net/z17q28cd/) you should see two text-areas and a 'Start Session' button
17+
18+
### Run ffmpeg-send with your browsers SessionDescription as stdin
19+
In the jsfiddle the top textarea is your browser, copy that and:
20+
#### Linux/macOS
21+
Run `echo $BROWSER_SDP | ffmpeg-send`
22+
#### Windows
23+
1. Paste the SessionDescription into a file.
24+
1. Run `ffmpeg-send < my_file`
25+
26+
### Input ffmpeg-send's SessionDescription into your browser
27+
Copy the text that `ffmpeg-send` just emitted and copy into second text area
28+
29+
### Hit 'Start Session' in jsfiddle, enjoy your video!
30+
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
31+
32+
Congrats, you have used Pion WebRTC! Now start building something cool

ffmpeg-send/main.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build !js
5+
// +build !js
6+
7+
// gstreamer-send is a simple application that shows how to send video to your browser using Pion WebRTC and GStreamer.
8+
package main
9+
10+
import (
11+
"errors"
12+
"fmt"
13+
"time"
14+
15+
"github.com/asticode/go-astiav"
16+
"github.com/pion/example-webrtc-applications/v3/internal/signal"
17+
"github.com/pion/webrtc/v3"
18+
"github.com/pion/webrtc/v3/pkg/media"
19+
)
20+
21+
func main() {
22+
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
23+
24+
// Prepare the configuration
25+
config := webrtc.Configuration{}
26+
27+
// Create a new RTCPeerConnection
28+
peerConnection, err := webrtc.NewPeerConnection(config)
29+
if err != nil {
30+
panic(err)
31+
}
32+
33+
// Set the handler for ICE connection state
34+
// This will notify you when the peer has connected/disconnected
35+
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
36+
fmt.Printf("Connection State has changed %s \n", connectionState.String())
37+
})
38+
39+
// Create a video track
40+
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/h264"}, "video", "pion")
41+
if err != nil {
42+
panic(err)
43+
}
44+
_, err = peerConnection.AddTrack(videoTrack)
45+
if err != nil {
46+
panic(err)
47+
}
48+
49+
// Wait for the offer to be pasted
50+
offer := webrtc.SessionDescription{}
51+
signal.Decode(signal.MustReadStdin(), &offer)
52+
53+
// Set the remote SessionDescription
54+
err = peerConnection.SetRemoteDescription(offer)
55+
if err != nil {
56+
panic(err)
57+
}
58+
59+
// Create an answer
60+
answer, err := peerConnection.CreateAnswer(nil)
61+
if err != nil {
62+
panic(err)
63+
}
64+
65+
// Create channel that is blocked until ICE Gathering is complete
66+
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
67+
68+
// Sets the LocalDescription, and starts our UDP listeners
69+
err = peerConnection.SetLocalDescription(answer)
70+
if err != nil {
71+
panic(err)
72+
}
73+
74+
<-gatherComplete
75+
76+
// Output the answer in base64 so we can paste it in browser
77+
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
78+
79+
// Start pushing buffers on these tracks
80+
writeH264ToTrack(videoTrack)
81+
82+
// Block forever
83+
select {}
84+
}
85+
86+
// nolint: gochecknoglobals
87+
var (
88+
inputFormatContext *astiav.FormatContext
89+
90+
decodeCodecContext *astiav.CodecContext
91+
decodePacket *astiav.Packet
92+
decodeFrame *astiav.Frame
93+
videoStream *astiav.Stream
94+
95+
softwareScaleContext *astiav.SoftwareScaleContext
96+
scaledFrame *astiav.Frame
97+
encodeCodecContext *astiav.CodecContext
98+
encodePacket *astiav.Packet
99+
100+
pts int64
101+
err error
102+
)
103+
104+
const h264FrameDuration = time.Millisecond * 20
105+
106+
func writeH264ToTrack(track *webrtc.TrackLocalStaticSample) {
107+
astiav.RegisterAllDevices()
108+
109+
initTestSrc()
110+
defer freeVideoCoding()
111+
112+
ticker := time.NewTicker(h264FrameDuration)
113+
for ; true; <-ticker.C {
114+
// Read frame from lavfi
115+
if err = inputFormatContext.ReadFrame(decodePacket); err != nil {
116+
if errors.Is(err, astiav.ErrEof) {
117+
break
118+
}
119+
panic(err)
120+
}
121+
122+
decodePacket.RescaleTs(videoStream.TimeBase(), decodeCodecContext.TimeBase())
123+
124+
// Send the packet
125+
if err = decodeCodecContext.SendPacket(decodePacket); err != nil {
126+
panic(err)
127+
}
128+
129+
for {
130+
// Read Decoded Frame
131+
if err = decodeCodecContext.ReceiveFrame(decodeFrame); err != nil {
132+
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
133+
break
134+
}
135+
panic(err)
136+
}
137+
138+
// Init the Scaling+Encoding. Can't be started until we know info on input video
139+
initVideoEncoding()
140+
141+
// Scale the video
142+
if err = softwareScaleContext.ScaleFrame(decodeFrame, scaledFrame); err != nil {
143+
panic(err)
144+
}
145+
146+
// We don't care about the PTS, but encoder complains if unset
147+
pts++
148+
scaledFrame.SetPts(pts)
149+
150+
// Encode the frame
151+
if err = encodeCodecContext.SendFrame(scaledFrame); err != nil {
152+
panic(err)
153+
}
154+
155+
for {
156+
// Read encoded packets and write to file
157+
encodePacket = astiav.AllocPacket()
158+
if err = encodeCodecContext.ReceivePacket(encodePacket); err != nil {
159+
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
160+
break
161+
}
162+
panic(err)
163+
}
164+
165+
// Write H264 to track
166+
if err = track.WriteSample(media.Sample{Data: encodePacket.Data(), Duration: h264FrameDuration}); err != nil {
167+
panic(err)
168+
}
169+
}
170+
}
171+
}
172+
}
173+
174+
func initTestSrc() {
175+
if inputFormatContext = astiav.AllocFormatContext(); inputFormatContext == nil {
176+
panic("Failed to AllocCodecContext")
177+
}
178+
179+
// Open input
180+
if err = inputFormatContext.OpenInput("testsrc=size=640x480:rate=30", astiav.FindInputFormat("lavfi"), nil); err != nil {
181+
panic(err)
182+
}
183+
184+
// Find stream info
185+
if err = inputFormatContext.FindStreamInfo(nil); err != nil {
186+
panic(err)
187+
}
188+
189+
videoStream = inputFormatContext.Streams()[0]
190+
191+
decodeCodec := astiav.FindDecoder(videoStream.CodecParameters().CodecID())
192+
if decodeCodec == nil {
193+
panic("FindDecoder returned nil")
194+
}
195+
196+
if decodeCodecContext = astiav.AllocCodecContext(decodeCodec); decodeCodecContext == nil {
197+
panic(err)
198+
}
199+
200+
if err = videoStream.CodecParameters().ToCodecContext(decodeCodecContext); err != nil {
201+
panic(err)
202+
}
203+
204+
decodeCodecContext.SetFramerate(inputFormatContext.GuessFrameRate(videoStream, nil))
205+
206+
if err = decodeCodecContext.Open(decodeCodec, nil); err != nil {
207+
panic(err)
208+
}
209+
210+
decodePacket = astiav.AllocPacket()
211+
decodeFrame = astiav.AllocFrame()
212+
}
213+
214+
func initVideoEncoding() {
215+
if encodeCodecContext != nil {
216+
return
217+
}
218+
219+
h264Encoder := astiav.FindEncoder(astiav.CodecIDH264)
220+
if h264Encoder == nil {
221+
panic("No H264 Encoder Found")
222+
}
223+
224+
if encodeCodecContext = astiav.AllocCodecContext(h264Encoder); encodeCodecContext == nil {
225+
panic("Failed to AllocCodecContext Decoder")
226+
}
227+
228+
encodeCodecContext.SetPixelFormat(astiav.PixelFormatYuv420P)
229+
encodeCodecContext.SetSampleAspectRatio(decodeCodecContext.SampleAspectRatio())
230+
encodeCodecContext.SetTimeBase(astiav.NewRational(1, 30))
231+
encodeCodecContext.SetWidth(decodeCodecContext.Width())
232+
encodeCodecContext.SetHeight(decodeCodecContext.Height())
233+
234+
if err = encodeCodecContext.Open(h264Encoder, nil); err != nil {
235+
panic(err)
236+
}
237+
238+
softwareScaleContext, err = astiav.CreateSoftwareScaleContext(
239+
decodeCodecContext.Width(),
240+
decodeCodecContext.Height(),
241+
decodeCodecContext.PixelFormat(),
242+
decodeCodecContext.Width(),
243+
decodeCodecContext.Height(),
244+
astiav.PixelFormatYuv420P,
245+
astiav.NewSoftwareScaleContextFlags(astiav.SoftwareScaleContextFlagBilinear),
246+
)
247+
if err != nil {
248+
panic(err)
249+
}
250+
251+
scaledFrame = astiav.AllocFrame()
252+
}
253+
254+
func freeVideoCoding() {
255+
inputFormatContext.CloseInput()
256+
inputFormatContext.Free()
257+
258+
decodeCodecContext.Free()
259+
decodePacket.Free()
260+
decodeFrame.Free()
261+
262+
scaledFrame.Free()
263+
softwareScaleContext.Free()
264+
encodeCodecContext.Free()
265+
encodePacket.Free()
266+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.22
55
toolchain go1.22.4
66

77
require (
8+
github.com/asticode/go-astiav v0.16.0
89
github.com/at-wat/ebml-go v0.17.1
910
github.com/emiago/sipgo v0.22.0
1011
github.com/go-gst/go-gst v1.0.0
@@ -21,6 +22,7 @@ require (
2122
)
2223

2324
require (
25+
github.com/asticode/go-astikit v0.42.0 // indirect
2426
github.com/davecgh/go-spew v1.1.1 // indirect
2527
github.com/go-gst/go-glib v1.0.0 // indirect
2628
github.com/gobwas/httphead v0.1.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/asticode/go-astiav v0.16.0 h1:z30tAx7GpnoKtXX1M6kxkoWb110kQqnp672iEaI7l70=
2+
github.com/asticode/go-astiav v0.16.0/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s=
3+
github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w=
4+
github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
15
github.com/at-wat/ebml-go v0.17.1 h1:pWG1NOATCFu1hnlowCzrA1VR/3s8tPY6qpU+2FwW7X4=
26
github.com/at-wat/ebml-go v0.17.1/go.mod h1:w1cJs7zmGsb5nnSvhWGKLCxvfu4FVx5ERvYDIalj1ww=
37
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=

0 commit comments

Comments
 (0)