Skip to main content
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.
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.

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

1

File Detection

The application checks for the existence of video and audio files:
_, err := os.Stat(videoFileName)
haveVideoFile := !os.IsNotExist(err)

_, err = os.Stat(audioFileName)
haveAudioFile := !os.IsNotExist(err)
2

Track Creation

Based on available files, the application creates corresponding local tracks:
videoTrack, err := webrtc.NewTrackLocalStaticSample(
    webrtc.RTPCodecCapability{MimeType: trackCodec},
    "video",
    "pion",
)
3

Connection Synchronization

Uses context to wait for ICE connection before streaming:
iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())

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

// In streaming goroutine
<-iceConnectedCtx.Done()
4

Frame Pacing

Uses time.Ticker for accurate frame timing without accumulating skew:
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...
}

Complete Source Code

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)
            }
        }
    }()
}

Important Implementation Details

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)
This ensures frames are sent at the correct rate without drift over time.
The example spawns goroutines to continuously read RTCP packets:
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.
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.

Running the Example

1

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

Start the application

cd examples/play-from-disk
go run main.go
3

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
4

Watch the stream

The video and audio will begin playing in your browser
For production applications, consider implementing adaptive bitrate streaming, handling seeking, and using a proper signaling mechanism instead of copy-paste.

Build docs developers (and LLMs) love