Skip to main content
The save-to-disk example demonstrates how to receive media streams from a web browser and save them to disk in standard formats. This is essential for building recording features, archiving video calls, or creating content for later playback.

Overview

This example receives VP8 video and Opus audio from a browser’s webcam/microphone and saves them as .ivf (video) and .ogg (audio) files. It showcases proper media pipeline configuration, RTP packet handling, and file writing.

Key Features

  • Custom MediaEngine configuration for specific codecs
  • Interceptor registry for RTCP features (NACK, PLI)
  • Automatic codec detection and file format selection
  • Graceful connection handling and file closure
  • Interval PLI for keyframe generation

How It Works

1

Configure MediaEngine

Create a custom MediaEngine and register only the codecs you need:
mediaEngine := &webrtc.MediaEngine{}

// Register VP8 for video
if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
    RTPCodecCapability: webrtc.RTPCodecCapability{
        MimeType:  webrtc.MimeTypeVP8,
        ClockRate: 90000,
        Channels:  0,
    },
    PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
    panic(err)
}

// Register Opus for audio
if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
    RTPCodecCapability: webrtc.RTPCodecCapability{
        MimeType:  webrtc.MimeTypeOpus,
        ClockRate: 48000,
        Channels:  0,
    },
    PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); err != nil {
    panic(err)
}
2

Setup Interceptors

Configure the RTP/RTCP pipeline with interceptors:
interceptorRegistry := &interceptor.Registry{}

// Add interval PLI interceptor
// Sends PLI every 3 seconds to generate keyframes
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
if err != nil {
    panic(err)
}
interceptorRegistry.Add(intervalPliFactory)

// Register default interceptors (NACK, RTCP reports, etc.)
if err = webrtc.RegisterDefaultInterceptors(
    mediaEngine,
    interceptorRegistry,
); err != nil {
    panic(err)
}
3

Create API and PeerConnection

Build a custom API instance with the configured MediaEngine:
api := webrtc.NewAPI(
    webrtc.WithMediaEngine(mediaEngine),
    webrtc.WithInterceptorRegistry(interceptorRegistry),
)

peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
    ICEServers: []webrtc.ICEServer{
        {
            URLs: []string{"stun:stun.l.google.com:19302"},
        },
    },
})
4

Add Transceivers

Explicitly add transceivers to receive audio and video:
// Allow us to receive 1 audio track and 1 video track
if _, err = peerConnection.AddTransceiverFromKind(
    webrtc.RTPCodecTypeAudio,
); err != nil {
    panic(err)
}

if _, err = peerConnection.AddTransceiverFromKind(
    webrtc.RTPCodecTypeVideo,
); err != nil {
    panic(err)
}
5

Create File Writers

Open file writers for audio and video:
oggFile, err := oggwriter.New("output.ogg", 48000, 2)
if err != nil {
    panic(err)
}

ivfFile, err := ivfwriter.New(
    "output.ivf",
    ivfwriter.WithCodec("video/VP8"),
)
if err != nil {
    panic(err)
}
6

Handle Incoming Tracks

Process incoming tracks and write to appropriate files:
peerConnection.OnTrack(func(
    track *webrtc.TrackRemote,
    receiver *webrtc.RTPReceiver,
) {
    codec := track.Codec()
    
    if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) {
        fmt.Println("Got Opus track, saving to output.ogg")
        saveToDisk(oggFile, track)
    } else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) {
        fmt.Println("Got VP8 track, saving to output.ivf")
        saveToDisk(ivfFile, track)
    }
})

Complete Source Code

func saveToDisk(writer media.Writer, track *webrtc.TrackRemote) {
    defer func() {
        if err := writer.Close(); err != nil {
            panic(err)
        }
    }()

    for {
        // Read RTP packets from the track
        rtpPacket, _, err := track.ReadRTP()
        if err != nil {
            fmt.Println(err)
            return
        }
        
        // Write the RTP packet to the file
        if err := writer.WriteRTP(rtpPacket); err != nil {
            fmt.Println(err)
            return
        }
    }
}

Important Implementation Details

The interval PLI (Picture Loss Indication) interceptor sends a PLI request every 3 seconds:
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
Benefits:
  • Forces generation of video keyframes periodically
  • Makes recorded video seekable
  • Improves error resilience
Trade-offs:
  • Lower picture quality (keyframes are larger)
  • Higher bitrates
In production, forward RTCP packets from viewers to senders instead of using interval PLI.
This example uses a custom MediaEngine instead of webrtc.NewPeerConnection():Custom MediaEngine allows:
  • Registering only specific codecs
  • Setting custom payload types
  • Fine-grained control over RTP parameters
When to use:
  • You need specific codec support
  • Interoperating with systems that require specific payload types
  • Building specialized media processing pipelines
The example uses Pion’s media writers:
  • IVF Writer: Container for VP8/VP9/AV1 video
  • OGG Writer: Container for Opus audio
For other formats, you can:
  1. Read RTP packets with track.ReadRTP()
  2. Extract payload data
  3. Write to your custom format
rtpPacket, _, err := track.ReadRTP()
payload := rtpPacket.Payload
// Write payload to your custom format
The InterceptorRegistry provides the RTP/RTCP processing pipeline:
interceptorRegistry := &interceptor.Registry{}
interceptorRegistry.Add(intervalPliFactory)
webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry)
Default interceptors include:
  • NACK (Negative Acknowledgment) for packet loss recovery
  • RTCP report generation
  • Statistics collection
When manually managing PeerConnections, you MUST create an InterceptorRegistry for each one.

Running the Example

1

Start the application

cd examples/save-to-disk
go run main.go
2

Open in browser

Navigate to http://localhost and select the Save to Disk example
3

Grant permissions

Allow the browser to access your webcam and microphone
4

Complete handshake

  1. Copy the offer from the browser
  2. Paste it into the terminal
  3. Copy the answer from terminal
  4. Paste it back into the browser
5

Record and stop

  • Recording starts automatically when connected
  • Press Ctrl+C in the browser to stop
  • Files output.ogg and output.ivf will be created

Output Files

After recording, you’ll have two files:
  • output.ogg: Opus audio at 48kHz, 2 channels
  • output.ivf: VP8 video with original dimensions and framerate
You can play these files back using:
For production recording, consider implementing:
  • Muxing audio and video into a single container (MP4, WebM)
  • Configurable quality settings
  • Storage management and cleanup
  • Recording session metadata

Build docs developers (and LLMs) love