> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pion/webrtc/llms.txt
> Use this file to discover all available pages before exploring further.

# Save to Disk

> Record webcam and microphone streams and save them to disk as VP8 and Opus files

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

<Steps>
  <Step title="Configure MediaEngine">
    Create a custom MediaEngine and register only the codecs you need:

    ```go theme={null}
    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)
    }
    ```
  </Step>

  <Step title="Setup Interceptors">
    Configure the RTP/RTCP pipeline with interceptors:

    ```go theme={null}
    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)
    }
    ```
  </Step>

  <Step title="Create API and PeerConnection">
    Build a custom API instance with the configured MediaEngine:

    ```go theme={null}
    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"},
            },
        },
    })
    ```
  </Step>

  <Step title="Add Transceivers">
    Explicitly add transceivers to receive audio and video:

    ```go theme={null}
    // 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)
    }
    ```
  </Step>

  <Step title="Create File Writers">
    Open file writers for audio and video:

    ```go theme={null}
    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)
    }
    ```
  </Step>

  <Step title="Handle Incoming Tracks">
    Process incoming tracks and write to appropriate files:

    ```go theme={null}
    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)
        }
    })
    ```
  </Step>
</Steps>

## Complete Source Code

<CodeGroup>
  ```go Save to Disk Function theme={null}
  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
          }
      }
  }
  ```

  ```go Connection Handling theme={null}
  // Set the handler for ICE connection state
  peerConnection.OnICEConnectionStateChange(func(
      connectionState webrtc.ICEConnectionState,
  ) {
      fmt.Printf("Connection State has changed %s \n", connectionState.String())

      if connectionState == webrtc.ICEConnectionStateConnected {
          fmt.Println("Ctrl+C the remote client to stop the demo")
      } else if connectionState == webrtc.ICEConnectionStateFailed ||
                connectionState == webrtc.ICEConnectionStateClosed {
          
          // Close files gracefully
          if closeErr := oggFile.Close(); closeErr != nil {
              panic(closeErr)
          }

          if closeErr := ivfFile.Close(); closeErr != nil {
              panic(closeErr)
          }

          fmt.Println("Done writing media files")

          // Gracefully shutdown the peer connection
          if closeErr := peerConnection.Close(); closeErr != nil {
              panic(closeErr)
          }

          os.Exit(0)
      }
  })
  ```

  ```go Main Setup theme={null}
  func main() {
      // Create MediaEngine
      mediaEngine := &webrtc.MediaEngine{}

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

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

      // Create InterceptorRegistry
      interceptorRegistry := &interceptor.Registry{}

      // Register interval PLI interceptor
      intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
      if err != nil {
          panic(err)
      }
      interceptorRegistry.Add(intervalPliFactory)

      // Use default interceptors
      if err = webrtc.RegisterDefaultInterceptors(
          mediaEngine,
          interceptorRegistry,
      ); err != nil {
          panic(err)
      }

      // Create API with MediaEngine
      api := webrtc.NewAPI(
          webrtc.WithMediaEngine(mediaEngine),
          webrtc.WithInterceptorRegistry(interceptorRegistry),
      )

      // Create PeerConnection
      peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
          ICEServers: []webrtc.ICEServer{
              {
                  URLs: []string{"stun:stun.l.google.com:19302"},
              },
          },
      })
      if err != nil {
          panic(err)
      }

      // Add transceivers
      if _, err = peerConnection.AddTransceiverFromKind(
          webrtc.RTPCodecTypeAudio,
      ); err != nil {
          panic(err)
      }
      
      if _, err = peerConnection.AddTransceiverFromKind(
          webrtc.RTPCodecTypeVideo,
      ); err != nil {
          panic(err)
      }

      // Create file writers
      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)
      }

      // Set track handler
      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 disk as output.ogg")
              saveToDisk(oggFile, track)
          } else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) {
              fmt.Println("Got VP8 track, saving to disk as output.ivf")
              saveToDisk(ivfFile, track)
          }
      })

      // ... Continue with signaling and connection setup
  }
  ```
</CodeGroup>

## Important Implementation Details

<AccordionGroup>
  <Accordion title="Why Use Interval PLI Interceptor?">
    The interval PLI (Picture Loss Indication) interceptor sends a PLI request every 3 seconds:

    ```go theme={null}
    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

    <Note>
      In production, forward RTCP packets from viewers to senders instead of using interval PLI.
    </Note>
  </Accordion>

  <Accordion title="Custom MediaEngine vs Default">
    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
  </Accordion>

  <Accordion title="File Format Support">
    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

    ```go theme={null}
    rtpPacket, _, err := track.ReadRTP()
    payload := rtpPacket.Payload
    // Write payload to your custom format
    ```
  </Accordion>

  <Accordion title="Interceptor Registry">
    The InterceptorRegistry provides the RTP/RTCP processing pipeline:

    ```go theme={null}
    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.**
  </Accordion>
</AccordionGroup>

## Running the Example

<Steps>
  <Step title="Start the application">
    ```bash theme={null}
    cd examples/save-to-disk
    go run main.go
    ```
  </Step>

  <Step title="Open in browser">
    Navigate to `http://localhost` and select the Save to Disk example
  </Step>

  <Step title="Grant permissions">
    Allow the browser to access your webcam and microphone
  </Step>

  <Step title="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
  </Step>

  <Step title="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
  </Step>
</Steps>

## 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:

* VLC Media Player
* FFmpeg: `ffmpeg -i output.ivf -i output.ogg output.mp4`
* The [play-from-disk example](/examples/media-streaming)

<Tip>
  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
</Tip>

## Related Examples

* [Play from Disk](/examples/media-streaming) - Play back recorded files
* [Broadcast](/examples/broadcast) - Forward streams to multiple viewers
* [RTP Forwarder](/examples/rtp-forwarder) - Forward RTP to external applications
