snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
This commit is contained in:
48
examples/quick-switch/README.md
Normal file
48
examples/quick-switch/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# quick-switch
|
||||
quick-switch demonstrates how to quickly switch between multiple videos using WebRTC.
|
||||
Similiar to how sites like TikTok quickly swipe between videos
|
||||
|
||||
|
||||
<video src="https://github.com/user-attachments/assets/07f15044-3aeb-44e2-a866-14bd4258aca0" autoplay loop muted> </video>
|
||||
|
||||
The logic in the frontend is purposefully kept as simple as possible. Making it easy to use this on any platform that supports WebRTC.
|
||||
|
||||
In the `main.go` we have one video track, and we switch which video file is written to it.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Download quick-switch
|
||||
This example requires you to clone the repo since it is serving static HTML.
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/webrtc.git
|
||||
cd webrtc/examples/quick-switch
|
||||
```
|
||||
|
||||
### Create your videos
|
||||
This example expects AV1 inside a ivf container. I used the follow to encode for my demo.
|
||||
|
||||
```
|
||||
ffmpeg -y \
|
||||
-i $YOUR_INPUT_VIDEO \
|
||||
-c:v libaom-av1 \
|
||||
-usage realtime \
|
||||
-lag-in-frames 0 \
|
||||
-crf 30 \
|
||||
-b:v 0 \
|
||||
-g 15 \
|
||||
-keyint_min 15 \
|
||||
-sc_threshold 0 \
|
||||
-pix_fmt yuv420p \
|
||||
-f ivf \
|
||||
output.ivf
|
||||
```
|
||||
|
||||
|
||||
### Run quick-switch
|
||||
Execute `go run *.go`
|
||||
|
||||
### Open the Web UI
|
||||
Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection.
|
||||
|
||||
Press 'Next Video' and have fun! If you have ideas on how to make it better we would love to hear.
|
||||
45
examples/quick-switch/index.html
Normal file
45
examples/quick-switch/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<html>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
<head>
|
||||
<title>quick-switch</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<video id="video" autoplay muted playsinline width="500" height="500"></video>
|
||||
<button id="nextVideo"> Next Video </button><br />
|
||||
</body>
|
||||
|
||||
<script>
|
||||
let peerConnection = new RTCPeerConnection()
|
||||
peerConnection.addTransceiver('video', {direction: 'recvonly'})
|
||||
peerConnection.ontrack = function (event) {
|
||||
document.getElementById('video').srcObject = event.streams[0]
|
||||
}
|
||||
|
||||
let dataChannel = peerConnection.createDataChannel('')
|
||||
dataChannel.onopen = () => {
|
||||
document.getElementById('nextVideo').onclick = () => {
|
||||
dataChannel.send('')
|
||||
}
|
||||
}
|
||||
|
||||
peerConnection.createOffer()
|
||||
.then(offer => peerConnection.setLocalDescription(offer)
|
||||
.then(() => fetch(`/whip`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/sdp'
|
||||
},
|
||||
body: offer.sdp
|
||||
}))
|
||||
)
|
||||
.then(res => res.text())
|
||||
.then(res => peerConnection.setRemoteDescription({type: 'answer', sdp: res}))
|
||||
.catch(alert)
|
||||
</script>
|
||||
|
||||
</html>
|
||||
188
examples/quick-switch/main.go
Normal file
188
examples/quick-switch/main.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
|
||||
// quick-switch demonstrates Pion WebRTC's ability to quickly switch between videos.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
"github.com/pion/webrtc/v4/pkg/media/ivfreader"
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
tracksLock sync.RWMutex
|
||||
tracks []*webrtc.TrackLocalStaticSample
|
||||
|
||||
videoFiles []string
|
||||
videoFileIndex atomic.Int32
|
||||
)
|
||||
|
||||
func nextVideo() {
|
||||
newIndex := videoFileIndex.Load() + 1
|
||||
if int(newIndex) >= len(videoFiles) {
|
||||
newIndex = 0
|
||||
}
|
||||
|
||||
videoFileIndex.Store(newIndex)
|
||||
}
|
||||
|
||||
// nolint: cyclop
|
||||
func doWHIP(res http.ResponseWriter, req *http.Request) {
|
||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
d.OnMessage(func(_ webrtc.DataChannelMessage) {
|
||||
nextVideo()
|
||||
})
|
||||
})
|
||||
|
||||
// One Track is used for PeerConnection. All video streams are written to one Track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeAV1,
|
||||
}, "video", "video")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tracksLock.Lock()
|
||||
tracks = append(tracks, videoTrack)
|
||||
tracksLock.Unlock()
|
||||
|
||||
// Set the handler for ICE connection state
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
|
||||
if connectionState == webrtc.ICEConnectionStateClosed || connectionState == webrtc.ICEConnectionStateFailed {
|
||||
if closeErr := peerConnection.Close(); closeErr != nil {
|
||||
panic(closeErr)
|
||||
}
|
||||
|
||||
tracksLock.Lock()
|
||||
tracks = slices.DeleteFunc(tracks, func(x *webrtc.TrackLocalStaticSample) bool { return x == videoTrack })
|
||||
tracksLock.Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
offer, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
SDP: string(offer),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create channel that is blocked until ICE Gathering is complete
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
||||
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Block until ICE Gathering is complete, disabling trickle ICE
|
||||
// we do this because we only can exchange one signaling message
|
||||
// in a production application you should exchange ICE Candidates via OnICECandidate
|
||||
<-gatherComplete
|
||||
|
||||
res.Header().Set("Content-Type", "application/sdp")
|
||||
if _, err := res.Write([]byte(peerConnection.LocalDescription().SDP)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func playFile(fileIndex int32) {
|
||||
file, err := os.Open(videoFiles[fileIndex])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close() // nolint: errcheck
|
||||
|
||||
ivf, header, err := ivfreader.NewWith(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
frameDuration := time.Duration(header.TimebaseNumerator) * time.Second / time.Duration(header.TimebaseDenominator)
|
||||
ticker := time.NewTicker(frameDuration)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
if fileIndex != videoFileIndex.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
frame, _, err := ivf.ParseNextFrame()
|
||||
if errors.Is(err, io.EOF) {
|
||||
nextVideo()
|
||||
|
||||
return
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tracksLock.RLock()
|
||||
for _, t := range tracks {
|
||||
if err = t.WriteSample(media.Sample{Data: frame, Duration: frameDuration}); err != nil {
|
||||
tracksLock.RUnlock()
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
tracksLock.RUnlock()
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/", http.FileServer(http.Dir(".")))
|
||||
http.HandleFunc("/whip", doWHIP)
|
||||
|
||||
// Switch between all ivf files in current directory
|
||||
go func() {
|
||||
files, err := filepath.Glob("*.ivf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, p := range files {
|
||||
videoFiles = append(videoFiles, filepath.Base(p))
|
||||
}
|
||||
if len(videoFiles) == 0 {
|
||||
panic("no .ivf files found in the working directory")
|
||||
}
|
||||
|
||||
for {
|
||||
playFile(videoFileIndex.Load())
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Println("Open http://localhost:8080 to access this demo")
|
||||
// nolint: gosec
|
||||
panic(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
Reference in New Issue
Block a user