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

# Broadcast

> Broadcast video to multiple peers using the SFU pattern with a single upload

The broadcast example demonstrates how to implement a Selective Forwarding Unit (SFU) pattern where one broadcaster uploads video once, and the server forwards it to multiple viewers. This is essential for building scalable video streaming applications, webinars, or live broadcasting platforms.

## Overview

This example creates a simple SFU (Selective Forwarding Unit) that accepts one video stream from a broadcaster and distributes it to multiple viewers. The broadcaster only uploads once, dramatically reducing bandwidth requirements compared to peer-to-peer mesh architectures.

## Key Features

* One-to-many video distribution
* Minimal bandwidth usage for broadcaster
* HTTP server for easy SDP exchange
* Automatic track forwarding to all connected viewers
* Support for unlimited viewers
* Interval PLI for keyframe management

## Architecture

```
Broadcaster → [Server/SFU] → Viewer 1
                    ↓
                 Viewer 2
                    ↓
                 Viewer 3
                    ↓
                   ...
```

## How It Works

<Steps>
  <Step title="Start HTTP Server">
    The example starts an HTTP server to receive SDP offers:

    ```go theme={null}
    func httpSDPServer(port int) chan string {
        sdpChan := make(chan string)
        http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
            body, _ := io.ReadAll(req.Body)
            fmt.Fprintf(res, "done")
            sdpChan <- string(body)
        })

        go func() {
            panic(http.ListenAndServe(":"+strconv.Itoa(port), nil))
        }()

        return sdpChan
    }
    ```
  </Step>

  <Step title="Accept Broadcaster Connection">
    Create the first PeerConnection to receive the broadcast:

    ```go theme={null}
    peerConnection, err := webrtc.NewAPI(
        webrtc.WithMediaEngine(mediaEngine),
        webrtc.WithInterceptorRegistry(interceptorRegistry),
    ).NewPeerConnection(peerConnectionConfig)

    // Allow us to receive 1 video track
    if _, err = peerConnection.AddTransceiverFromKind(
        webrtc.RTPCodecTypeVideo,
    ); err != nil {
        panic(err)
    }
    ```
  </Step>

  <Step title="Create Local Track for Distribution">
    When receiving the remote track, create a local track to forward to viewers:

    ```go theme={null}
    localTrackChan := make(chan *webrtc.TrackLocalStaticRTP)

    peerConnection.OnTrack(func(
        remoteTrack *webrtc.TrackRemote,
        receiver *webrtc.RTPReceiver,
    ) {
        // Create a local track for distribution
        localTrack, err := webrtc.NewTrackLocalStaticRTP(
            remoteTrack.Codec().RTPCodecCapability,
            "video",
            "pion",
        )
        if err != nil {
            panic(err)
        }
        localTrackChan <- localTrack

        // Forward all packets from remote to local
        rtpBuf := make([]byte, 1400)
        for {
            i, _, readErr := remoteTrack.Read(rtpBuf)
            if readErr != nil {
                panic(readErr)
            }

            // Write to local track (forwards to all viewers)
            if _, err = localTrack.Write(rtpBuf[:i]); err != nil &&
               !errors.Is(err, io.ErrClosedPipe) {
                panic(err)
            }
        }
    })
    ```
  </Step>

  <Step title="Add Viewers">
    For each viewer, create a new PeerConnection and add the local track:

    ```go theme={null}
    for {
        fmt.Println("Curl an base64 SDP to start sendonly peer connection")

        recvOnlyOffer := webrtc.SessionDescription{}
        decode(<-sdpChan, &recvOnlyOffer)

        // Create a new PeerConnection for this viewer
        peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig)
        if err != nil {
            panic(err)
        }

        // Add the broadcast track to this viewer
        rtpSender, err := peerConnection.AddTrack(localTrack)
        if err != nil {
            panic(err)
        }

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

        // Complete connection setup (create answer, etc.)
        // ...
    }
    ```
  </Step>
</Steps>

## Complete Source Code

<CodeGroup>
  ```go Main Application theme={null}
  package main

  import (
      "encoding/base64"
      "encoding/json"
      "errors"
      "flag"
      "fmt"
      "io"
      "net/http"
      "strconv"

      "github.com/pion/interceptor"
      "github.com/pion/interceptor/pkg/intervalpli"
      "github.com/pion/webrtc/v4"
  )

  func main() {
      port := flag.Int("port", 8080, "http server port")
      flag.Parse()

      sdpChan := httpSDPServer(*port)

      // Get broadcaster's offer
      offer := webrtc.SessionDescription{}
      decode(<-sdpChan, &offer)
      fmt.Println("")

      peerConnectionConfig := webrtc.Configuration{
          ICEServers: []webrtc.ICEServer{
              {
                  URLs: []string{"stun:stun.l.google.com:19302"},
              },
          },
      }

      mediaEngine := &webrtc.MediaEngine{}
      if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
          panic(err)
      }

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

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

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

      // Create PeerConnection for broadcaster
      peerConnection, err := webrtc.NewAPI(
          webrtc.WithMediaEngine(mediaEngine),
          webrtc.WithInterceptorRegistry(interceptorRegistry),
      ).NewPeerConnection(peerConnectionConfig)
      if err != nil {
          panic(err)
      }
      defer peerConnection.Close()

      // Allow receiving 1 video track
      if _, err = peerConnection.AddTransceiverFromKind(
          webrtc.RTPCodecTypeVideo,
      ); err != nil {
          panic(err)
      }

      localTrackChan := make(chan *webrtc.TrackLocalStaticRTP)

      // Handle incoming track from broadcaster
      peerConnection.OnTrack(func(
          remoteTrack *webrtc.TrackRemote,
          receiver *webrtc.RTPReceiver,
      ) {
          // Create local track for distribution
          localTrack, err := webrtc.NewTrackLocalStaticRTP(
              remoteTrack.Codec().RTPCodecCapability,
              "video",
              "pion",
          )
          if err != nil {
              panic(err)
          }
          localTrackChan <- localTrack

          rtpBuf := make([]byte, 1400)
          for {
              i, _, readErr := remoteTrack.Read(rtpBuf)
              if readErr != nil {
                  panic(readErr)
              }

              // Forward to all connected viewers
              if _, err = localTrack.Write(rtpBuf[:i]); err != nil &&
                 !errors.Is(err, io.ErrClosedPipe) {
                  panic(err)
              }
          }
      })

      // Set remote description and create answer for broadcaster
      err = peerConnection.SetRemoteDescription(offer)
      if err != nil {
          panic(err)
      }

      answer, err := peerConnection.CreateAnswer(nil)
      if err != nil {
          panic(err)
      }

      gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
      err = peerConnection.SetLocalDescription(answer)
      if err != nil {
          panic(err)
      }
      <-gatherComplete

      fmt.Println(encode(peerConnection.LocalDescription()))

      localTrack := <-localTrackChan

      // Accept viewer connections
      for {
          fmt.Println("")
          fmt.Println("Curl an base64 SDP to start sendonly peer connection")

          recvOnlyOffer := webrtc.SessionDescription{}
          decode(<-sdpChan, &recvOnlyOffer)

          // Create PeerConnection for viewer
          peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig)
          if err != nil {
              panic(err)
          }

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

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

          err = peerConnection.SetRemoteDescription(recvOnlyOffer)
          if err != nil {
              panic(err)
          }

          answer, err := peerConnection.CreateAnswer(nil)
          if err != nil {
              panic(err)
          }

          gatherComplete = webrtc.GatheringCompletePromise(peerConnection)
          err = peerConnection.SetLocalDescription(answer)
          if err != nil {
              panic(err)
          }
          <-gatherComplete

          fmt.Println(encode(peerConnection.LocalDescription()))
      }
  }
  ```

  ```go Helper Functions theme={null}
  func encode(obj *webrtc.SessionDescription) string {
      b, err := json.Marshal(obj)
      if err != nil {
          panic(err)
      }
      return base64.StdEncoding.EncodeToString(b)
  }

  func decode(in string, obj *webrtc.SessionDescription) {
      b, err := base64.StdEncoding.DecodeString(in)
      if err != nil {
          panic(err)
      }

      if err = json.Unmarshal(b, obj); err != nil {
          panic(err)
      }
  }

  func httpSDPServer(port int) chan string {
      sdpChan := make(chan string)
      http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
          body, _ := io.ReadAll(req.Body)
          fmt.Fprintf(res, "done")
          sdpChan <- string(body)
      })

      go func() {
          panic(http.ListenAndServe(":"+strconv.Itoa(port), nil))
      }()

      return sdpChan
  }
  ```
</CodeGroup>

## Important Implementation Details

<AccordionGroup>
  <Accordion title="TrackLocalStaticRTP">
    The example uses `TrackLocalStaticRTP` to forward packets:

    ```go theme={null}
    localTrack, err := webrtc.NewTrackLocalStaticRTP(
        remoteTrack.Codec().RTPCodecCapability,
        "video",
        "pion",
    )
    ```

    **Why StaticRTP?**

    * Direct packet forwarding without re-encoding
    * Low CPU usage
    * Preserves original quality
    * No transcoding overhead

    The same local track instance is added to all viewer PeerConnections.
  </Accordion>

  <Accordion title="ErrClosedPipe Handling">
    When no viewers are connected, writing to the local track returns `io.ErrClosedPipe`:

    ```go theme={null}
    if _, err = localTrack.Write(rtpBuf[:i]); err != nil &&
       !errors.Is(err, io.ErrClosedPipe) {
        panic(err)
    }
    ```

    This is expected and should not cause the application to panic. Once viewers connect, packets will be successfully delivered.
  </Accordion>

  <Accordion title="HTTP Signaling">
    This example uses HTTP POST for signaling:

    ```bash theme={null}
    # Send SDP offer
    curl -X POST -d "<base64-encoded-sdp>" http://localhost:8080
    ```

    For production, implement:

    * WebSocket signaling for bidirectional communication
    * Authentication and authorization
    * Session management
    * Proper error handling
  </Accordion>

  <Accordion title="Scalability Considerations">
    **Current implementation:**

    * Single server process
    * Unlimited viewers (limited by server resources)
    * No load balancing

    **For production scale:**

    * Implement clustering with multiple SFU servers
    * Use cascading SFUs for geographic distribution
    * Add bandwidth estimation and adaptive bitrate
    * Monitor server resources and connection health
    * Consider using established SFU solutions like [Pion SFU](https://github.com/pion/ion-sfu)
  </Accordion>
</AccordionGroup>

## Running the Example

<Steps>
  <Step title="Start the server">
    ```bash theme={null}
    cd examples/broadcast
    go run main.go --port 8080
    ```
  </Step>

  <Step title="Connect broadcaster">
    1. Open the broadcast page in a browser
    2. Enable your webcam
    3. Generate and copy the offer
    4. Send it via curl:

    ```bash theme={null}
    curl -X POST -d "<base64-sdp-offer>" http://localhost:8080
    ```

    5. Copy the answer from terminal and paste in browser
  </Step>

  <Step title="Connect viewers">
    For each viewer:

    1. Open the viewer page in a new browser/tab
    2. Generate and copy the offer
    3. Send via curl (same command as broadcaster)
    4. Copy the answer and paste in browser
    5. Video should start playing
  </Step>

  <Step title="Add more viewers">
    Repeat step 3 for as many viewers as needed. Each viewer receives the same broadcast stream.
  </Step>
</Steps>

## Bandwidth Comparison

<CardGroup cols={2}>
  <Card title="Mesh (P2P)" icon="diagram-project">
    **For 10 viewers:**

    * Broadcaster upload: 10x bandwidth
    * Each viewer: 1x bandwidth
    * Total: 20x bandwidth
  </Card>

  <Card title="SFU (This Example)" icon="server">
    **For 10 viewers:**

    * Broadcaster upload: 1x bandwidth
    * Server processing: Packet forwarding only
    * Total: 11x bandwidth (10x viewer download + 1x upload)
  </Card>
</CardGroup>

<Tip>
  The SFU pattern dramatically reduces broadcaster bandwidth requirements. With 100 viewers, mesh would require 100x upload bandwidth, while SFU still only requires 1x!
</Tip>

## Use Cases

* Live streaming events and webinars
* Video conferencing (with modifications for multi-publisher)
* Online education and virtual classrooms
* Live sports broadcasting
* Concert and performance streaming

## Related Examples

* [Play from Disk](/examples/media-streaming) - Stream pre-recorded content
* [Save to Disk](/examples/save-to-disk) - Record the broadcast
* [Simulcast](/examples/simulcast) - Handle multiple quality streams
