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

# Play from Disk

> Send video and audio files from disk to your browser using Pion WebRTC

The play-from-disk example demonstrates how to stream pre-recorded video and audio files to a web browser using WebRTC. This is useful for VOD (Video on Demand) applications, testing, or any scenario where you need to stream media files.

## Overview

This example reads VP8/VP9/AV1 video (.ivf) and Opus audio (.ogg) files from disk and streams them to a connected peer in real-time, maintaining proper pacing to match the original playback speed.

<Note>
  The example expects files named `output.ogg` (audio) and `output.ivf` (video) in the working directory. You can generate these files using the [save-to-disk example](/examples/save-to-disk).
</Note>

## Key Features

* Supports multiple video codecs (VP8, VP9, AV1)
* Proper frame pacing using time.Ticker
* Handles both audio and video tracks independently
* Demonstrates RTCP packet processing
* Uses context for connection synchronization

## How It Works

<Steps>
  <Step title="File Detection">
    The application checks for the existence of video and audio files:

    ```go theme={null}
    _, err := os.Stat(videoFileName)
    haveVideoFile := !os.IsNotExist(err)

    _, err = os.Stat(audioFileName)
    haveAudioFile := !os.IsNotExist(err)
    ```
  </Step>

  <Step title="Track Creation">
    Based on available files, the application creates corresponding local tracks:

    ```go theme={null}
    videoTrack, err := webrtc.NewTrackLocalStaticSample(
        webrtc.RTPCodecCapability{MimeType: trackCodec},
        "video",
        "pion",
    )
    ```
  </Step>

  <Step title="Connection Synchronization">
    Uses context to wait for ICE connection before streaming:

    ```go theme={null}
    iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())

    // In ICE handler
    if connectionState == webrtc.ICEConnectionStateConnected {
        iceConnectedCtxCancel()
    }

    // In streaming goroutine
    <-iceConnectedCtx.Done()
    ```
  </Step>

  <Step title="Frame Pacing">
    Uses time.Ticker for accurate frame timing without accumulating skew:

    ```go theme={null}
    ticker := time.NewTicker(
        time.Millisecond * time.Duration(
            (float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000,
        ),
    )
    defer ticker.Stop()
    for ; true; <-ticker.C {
        frame, _, err := ivf.ParseNextFrame()
        // Send frame...
    }
    ```
  </Step>
</Steps>

## Complete Source Code

<CodeGroup>
  ```go Video Streaming Logic theme={null}
  if haveVideoFile {
      file, err := os.Open(videoFileName)
      if err != nil {
          panic(err)
      }

      _, header, err := ivfreader.NewWith(file)
      if err != nil {
          panic(err)
      }

      // Determine video codec
      var trackCodec string
      switch header.FourCC {
      case "AV01":
          trackCodec = webrtc.MimeTypeAV1
      case "VP90":
          trackCodec = webrtc.MimeTypeVP9
      case "VP80":
          trackCodec = webrtc.MimeTypeVP8
      default:
          panic(fmt.Sprintf("Unable to handle FourCC %s", header.FourCC))
      }

      // Create a video track
      videoTrack, err := webrtc.NewTrackLocalStaticSample(
          webrtc.RTPCodecCapability{MimeType: trackCodec},
          "video",
          "pion",
      )
      if err != nil {
          panic(err)
      }

      rtpSender, err := peerConnection.AddTrack(videoTrack)
      if err != nil {
          panic(err)
      }

      // Read incoming RTCP packets
      go func() {
          rtcpBuf := make([]byte, 1500)
          for {
              if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
                  return
              }
          }
      }()

      go func() {
          // Wait for connection
          <-iceConnectedCtx.Done()

          // Stream video with proper pacing
          ticker := time.NewTicker(
              time.Millisecond * time.Duration(
                  (float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000,
              ),
          )
          defer ticker.Stop()
          
          for ; true; <-ticker.C {
              frame, _, err := ivf.ParseNextFrame()
              if errors.Is(err, io.EOF) {
                  fmt.Printf("All video frames parsed and sent")
                  os.Exit(0)
              }
              if err != nil {
                  panic(err)
              }
              if err = videoTrack.WriteSample(media.Sample{
                  Data:     frame,
                  Duration: time.Second,
              }); err != nil {
                  panic(err)
              }
          }
      }()
  }
  ```

  ```go Audio Streaming Logic theme={null}
  if haveAudioFile {
      // Create audio track
      audioTrack, err := webrtc.NewTrackLocalStaticSample(
          webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus},
          "audio",
          "pion",
      )
      if err != nil {
          panic(err)
      }

      rtpSender, err := peerConnection.AddTrack(audioTrack)
      if err != nil {
          panic(err)
      }

      // Read incoming RTCP packets
      go func() {
          rtcpBuf := make([]byte, 1500)
          for {
              if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
                  return
              }
          }
      }()

      go func() {
          file, err := os.Open(audioFileName)
          if err != nil {
              panic(err)
          }

          ogg, _, err := oggreader.NewWith(file)
          if err != nil {
              panic(err)
          }

          // Wait for connection
          <-iceConnectedCtx.Done()

          var lastGranule uint64
          ticker := time.NewTicker(20 * time.Millisecond)
          defer ticker.Stop()
          
          for ; true; <-ticker.C {
              pageData, pageHeader, err := ogg.ParseNextPage()
              if errors.Is(err, io.EOF) {
                  fmt.Printf("All audio pages parsed and sent")
                  os.Exit(0)
              }
              if err != nil {
                  panic(err)
              }

              // Calculate sample duration
              sampleCount := float64(pageHeader.GranulePosition - lastGranule)
              lastGranule = pageHeader.GranulePosition
              sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond

              if err = audioTrack.WriteSample(media.Sample{
                  Data:     pageData,
                  Duration: sampleDuration,
              }); err != nil {
                  panic(err)
              }
          }
      }()
  }
  ```
</CodeGroup>

## Important Implementation Details

<AccordionGroup>
  <Accordion title="Why use time.Ticker instead of time.Sleep?">
    The example uses `time.Ticker` instead of `time.Sleep` for two critical reasons:

    1. **Prevents skew accumulation**: `time.Sleep` doesn't compensate for time spent parsing and processing data
    2. **Avoids latency issues**: Works around known latency issues with `time.Sleep` (see [Go issue #44343](https://github.com/golang/go/issues/44343))

    This ensures frames are sent at the correct rate without drift over time.
  </Accordion>

  <Accordion title="RTCP Packet Reading">
    The example spawns goroutines to continuously read RTCP packets:

    ```go theme={null}
    go func() {
        rtcpBuf := make([]byte, 1500)
        for {
            if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
                return
            }
        }
    }()
    ```

    This is essential because RTCP packets are processed by interceptors before being returned. Features like NACK (Negative Acknowledgment) require this continuous reading.
  </Accordion>

  <Accordion title="Codec Detection">
    The example automatically detects the video codec from the IVF header:

    * `AV01` → AV1
    * `VP90` → VP9
    * `VP80` → VP8

    This allows the same code to handle different video formats without modification.
  </Accordion>
</AccordionGroup>

## Running the Example

<Steps>
  <Step title="Prepare media files">
    Ensure you have `output.ogg` and/or `output.ivf` in your working directory. You can create these using the [save-to-disk example](/examples/save-to-disk).
  </Step>

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

  <Step title="Complete the WebRTC handshake">
    1. Open the example in your browser (via the examples server)
    2. Copy the offer from the browser
    3. Paste it into the terminal
    4. Copy the answer from the terminal
    5. Paste it back into the browser
  </Step>

  <Step title="Watch the stream">
    The video and audio will begin playing in your browser
  </Step>
</Steps>

<Tip>
  For production applications, consider implementing adaptive bitrate streaming, handling seeking, and using a proper signaling mechanism instead of copy-paste.
</Tip>

## Related Examples

* [Save to Disk](/examples/save-to-disk) - Record media to create playable files
* [Broadcast](/examples/broadcast) - Stream to multiple viewers
* [RTP Forwarder](/examples/rtp-forwarder) - Forward streams to external applications
