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

# Produce a Track

Producing a track involves writing media data to a `moqt.TrackWriter`, which manages the creation of groups and frames for transmission.
`moqt.TrackWriter` is responsible for handling the track's lifecycle, including opening and closing groups.
`moqt.TrackWriter` is created when being subscribed to a track from the peer on the session. Created `moqt.TrackWriter` is then routed via the session's `moqt.TrackMux` and can be used to publish media data.

<Accordion title="Overview">
  ```go theme={null}
      mux := moqt.NewTrackMux(0)

      // Set the mux to a session
      // This differs from if it is client-side or server-side

      mux.PublishFunc(ctx, "/broadcast_path", func(tw *moqt.TrackWriter) {
          defer tw.Close() // Always close when done

          for {
              gw, err := tw.OpenGroup(ctx) // Create a new group
              if err != nil {
                  // Handle error
                  return
              }

              frame := moqt.NewFrame(1024)
              payload := []byte("sample data")
              frame.Write(payload)

              err = gw.WriteFrame(frame)
              if err != nil {
                  // Handle error
                  return
              }
          }
      })
  ```
</Accordion>

## Handle Track Subscriptions

When the other side (client or server) subscribes to a broadcast path, the registered `moqt.TrackHandler` is invoked to serve the subscription. The handler receives a `moqt.TrackWriter` and is responsible for sending the appropriate track data for the requested track name.

<Columns cols={2}>
  <Card title="Handle Track Subscriptions" icon="arrow-up-right-from-square" href="/moq/publish#handle-track-subscriptions" />
</Columns>

```go theme={null}
    var mux *moqt.TrackMux

    // Register the track handler with PublishFunc
    mux.PublishFunc(ctx, "/broadcast_path", func(tw *moqt.TrackWriter) {
        defer tw.Close() // Always close when done

        // Handle track subscription
    })
```

```go theme={null}
    // trackHandler implements moqt.TrackHandler
    var _ moqt.TrackHandler = (*trackHandler)(nil)

    type trackHandler struct{}

    func (h *trackHandler) ServeTrack(tw *moqt.TrackWriter) {
        defer tw.Close() // Always close when done

        // Handle track subscription
    }

    var mux *moqt.TrackMux

    // Register the track handler
    mux.Publish(ctx, "/broadcast_path", &trackHandler{})

    // Register the track handler using an Announcement
    var ann *moqt.Announcement
    mux.Announce(ann, &trackHandler{})
```

## Create a Group

To start a new group within a track, use `TrackWriter.OpenGroup` method. This creates a new group with an automatically incremented sequence number and returns a `moqt.GroupWriter` for writing frames to that group.

For explicit sequence control, use `TrackWriter.OpenGroupAt` method with a specific `moqt.GroupSequence`.

```go theme={null}
    var tw *moqt.TrackWriter
    gw, err := tw.OpenGroup(context.Background()) // Auto-incrementing sequence
    if err != nil {
        // Handle error
        return
    }
    defer gw.Close() // Always close when done
    // Use gw to write frames
```

```go theme={null}
    // Or use OpenGroupAt for explicit sequence control
    gw, err := tw.OpenGroupAt(context.Background(), moqt.GroupSequence(42))
    if err != nil {
        // Handle error
        return
    }
```

<Note>
  **Note: Context and Backpressure**

  `OpenGroup`/`OpenGroupAt` take a `context.Context` as their first argument. The group is opened on a fresh QUIC stream; when the peer's concurrent-stream limit (MAX\_STREAMS) is reached, the call blocks — applying flow-control backpressure — until a stream is granted or the context is done (either the passed context or the track's lifetime context, which is monitored internally). Pass `context.Background()` to bound the open to the track's lifetime, or a context with timeout to drop a group under pressure rather than wait.
</Note>

<Note>
  **Note: Group Ordering**

  Groups are supposed to be produced in order, with each group having a unique increasing sequence number and then consumed in order by the subscriber.
  However, groups not in order can be created, but this may lead to complications in playback and synchronization.
</Note>

<Note>
  **Note: Sequence Number 0**

  Sequence number 0 has a special meaning: it is reserved for special identifiers like "Latest Available Group" or "Final Group", and does not represent a real group.
</Note>

## Write Frames

To add media data to a group, use `GroupWriter.WriteFrame` method. Each frame represents a chunk of media data (audio, video, etc.) to be sent as part of the group.

```go theme={null}
    var gw *moqt.GroupWriter
    frame := moqt.NewFrame(0)
    payload := []byte("sample payload")
    frame.Write(payload) // Populate payload
    err := gw.WriteFrame(frame)
    if err != nil {
        // Handle error
    }
```

### Frame Creation

Create frames with `moqt.NewFrame(capacity)`, reuse the buffer with `Reset`, and append payload via `Write` or by manipulating `Body()` when needed.

```go theme={null}
    frame := moqt.NewFrame(1024)
    frame.Write([]byte("Good Morning"))
    fmt.Println(string(frame.Body())) // "Good Morning"

    frame.Reset() // Reuse buffer for next frame
    frame.Write([]byte("Good Evening"))
    fmt.Println(string(frame.Body())) // "Good Evening"
```

Frames are reused for efficiency; clone with `frame.Clone()` when you need to retain a copy after the next write.

> Relays MUST NOT combine, split, or otherwise modify object payloads.<br />
> — <cite>MOQT WG[^1]</cite>

[^1]: [IETF Draft - The Media Over QUIC Transport (moqtransport)](https://www.ietf.org/archive/id/draft-ietf-moq-transport-13.html)

## Finalize a Group

To finalize a group and indicate that no more frames will be sent, call `GroupWriter.Close` method.

```go theme={null}
    var gw *moqt.GroupWriter
    gw.Close()
```

## Cancel Group Writing

To cancel a group and stop sending frames, call `GroupWriter.CancelWrite` method with an error code.

```go theme={null}
    var group *moqt.GroupWriter
    var code moqt.GroupErrorCode
    group.CancelWrite(code)
```

## Stop a Track

To stop a track and indicate that no more groups will exist, call `TrackWriter.Close` method.

```go theme={null}
    var tw *moqt.TrackWriter
    tw.Close()
```

To abort a track and indicate that no more groups will exist due to an error, call `TrackWriter.CloseWithError` method with an error code.

```go theme={null}
    var tw *moqt.TrackWriter
    var code moqt.SubscribeErrorCode
    tw.CloseWithError(code)
```

* **Subscribe Error Code**:
  The subscribe error code is used to indicate the reason for closing the track subscription. It helps the publisher understand why the subscription finished.

If any groups belong to the track, they will be closed automatically with an error code `moqt.PublishAbortedErrorCode` when the track is closed. This ensures that all resources are released properly and no data is lost.

## Track Info

A subscriber can ask about a track's immutable publisher properties — priority, ordering preference, max latency, and timescale — via `Session.TrackInfo`, without subscribing to it. By default, `TrackMux` answers TRACK\_INFO requests with zero-valued properties (zero priority, unordered, no retention, `DefaultTimescale`).

To advertise real properties, implement `TrackInfoProvider` on your `TrackHandler`:

```go theme={null}
type trackHandler struct{}

var _ moqt.TrackHandler = (*trackHandler)(nil)
var _ moqt.TrackInfoProvider = (*trackHandler)(nil)

func (h *trackHandler) ServeTrack(tw *moqt.TrackWriter) {
	defer tw.Close()
	// Handle track subscription
}

// TrackInfo returns the publisher properties for the named track and
// whether the track exists.
func (h *trackHandler) TrackInfo(name moqt.TrackName) (moqt.PublishInfo, bool) {
	return moqt.PublishInfo{
		Priority:   128,
		Ordered:    true,
		MaxLatency: 2000,
		Timescale:  1000,
	}, true
}
```

TRACK\_INFO is intentionally out-of-band from the catalog: catalog fields (`msf.Track`) describe *content*, while `PublishInfo` carries *transport scheduling* hints, and the two live in different layers by design — `msf.Track` has no `Priority`/`Ordered` fields to source them from.

The `TrackInfoProvider` type assertion runs against the handler registered with `mux.Publish` for the broadcast path — not against the per-track handler passed to `RegisterTrack`. `msf.Broadcast` does not implement `TrackInfoProvider` itself, so publishing one directly means every track reports the default zero-valued properties. To supply real ones, embed `*msf.Broadcast` in your own type and implement `TrackInfo` on it, then publish that type instead of the `*msf.Broadcast` value:

```go theme={null}
type broadcast struct {
	*msf.Broadcast
	info map[moqt.TrackName]moqt.PublishInfo
}

// TrackInfo implements moqt.TrackInfoProvider. *msf.Broadcast already
// satisfies moqt.TrackHandler (ServeTrack is promoted), so this is the
// only method the wrapper needs to add.
func (b *broadcast) TrackInfo(name moqt.TrackName) (moqt.PublishInfo, bool) {
	info, ok := b.info[name]
	return info, ok
}

// mux.Publish(ctx, "/broadcast_path", &broadcast{Broadcast: msfBroadcast, info: infoByTrack})
```

`msf.Track` has spec-defined `TargetLatency`/`Timescale` fields that already carry the same meaning as `PublishInfo.MaxLatency`/`Timescale`, so a wrapper can forward those straight from the catalog. `Priority`/`Ordered` have no catalog equivalent to source from — they are tracked in [issue #420](https://github.com/qumo-dev/gomoqt/issues/420) as a possible future vendor extension to the catalog.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.