Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/release-kt-lib.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ jobs:
# Picked up by com.vanniktech.maven.publish via Gradle project properties.
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
# Signing credentials are scoped to the real publish step below, NOT here:
# fork PRs can't read repo secrets, and an empty `signingInMemoryKey` makes
# signAllPublications() attempt (and fail) instead of no-op. The dry-run
# doesn't publish, so it doesn't need to sign.

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down Expand Up @@ -150,6 +152,8 @@ jobs:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
env:
MOQ_VERSION: ${{ needs.check-version.outputs.version }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
run: |
gradle -p kt \
-Pmoq.version="$MOQ_VERSION" \
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/layer/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ But if a publisher needs a feature, then the subscriber needs it too, so you can

- **No Request IDs**: A bidirectional stream for each request to avoid HoLB. (NOTE: likely to be upstreamed into moq-transport)
- **No Push**: A subscriber must explicitly subscribe to each track.
- **No FETCH**: Use HTTP for VOD instead of reinventing the wheel.
- **Single-group FETCH only (lite-05+)**: Fetch one complete group by sequence. Ranges and joining fetches are not supported.
- **No Joining Fetch**: Subscriptions start at the latest group, not the latest frame.
- **No sub-groups**: SVC layers should be separate tracks.
- **No gaps**: Makes life much easier for the relay and every application.
Expand Down
38 changes: 38 additions & 0 deletions doc/lib/go/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,44 @@ broadcast.Finish()

If a producer is collected without `Finish()`, the underlying library logs a warning (`broadcast::Producer dropped without close()`) to help you spot the leak.

## Fetching raw groups

Fetch retrieves one group by track name and group sequence without keeping a live subscription:

```go
group, err := consumer.FetchGroup("events", 42, &moq.FetchGroupOptions{Priority: 10})
if err != nil {
log.Fatal(err)
}
for frame, err := range group.Frames(ctx) {
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", frame)
}
```

A retained group resolves immediately. To serve a group that is not retained, keep a dynamic handler alive on its producer:

```go
dynamic, err := track.Dynamic()
if err != nil {
log.Fatal(err)
}
request, err := dynamic.RequestedGroup(ctx)
if err != nil {
log.Fatal(err)
}
producer, err := request.Accept()
if err != nil {
log.Fatal(err)
}
_ = producer.WriteFrame(loadArchivedFrame(request.Sequence()))
_ = producer.Finish()
```

Call `request.Abort(code)` when the requested group cannot be produced. Fetch is currently a single-group operation and is supported by the moq-lite 05+ FETCH wire path.

## Local development

The in-tree `go/wrapper/` directory is the source skeleton; CI publishes it to the [moq-dev/moq-go](https://github.com/moq-dev/moq-go) mirror. To exercise it locally:
Expand Down
29 changes: 29 additions & 0 deletions doc/lib/kt/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,35 @@ Moq.connect("https://relay.example.com").use { moq ->
}
```

### Fetching raw groups

Fetch retrieves one group by track name and group sequence without keeping a live subscription:

```kotlin
val group = consumer.fetchGroup(
"events",
42uL,
FetchGroupOptions(priority = 10u),
)
group.frames().collect { frame ->
println(frame.decodeToString())
}
```

A retained group resolves immediately. To serve a group that is not retained, keep a dynamic handler alive on its producer:

```kotlin
val dynamic = track.dynamic()

dynamic.requestedGroups().collect { request ->
val group = request.accept()
group.writeFrame(loadArchivedFrame(request.sequence()))
group.finish()
}
```

Call `request.abort(code)` when the requested group cannot be produced. Fetch is currently a single-group operation and is supported by the moq-lite 05+ FETCH wire path.

### On-demand raw tracks

Use a dynamic broadcast when subscribers should be able to request raw tracks that are not published yet:
Expand Down
27 changes: 27 additions & 0 deletions doc/lib/py/moq-rs.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,33 @@ async for group in track:

`write_frame` on a track creates a one-frame group by default. Use `append_group()` for multi-frame groups (e.g., a video GOP).

### Fetching raw groups

Fetch retrieves one group by track name and group sequence without keeping a live subscription:

```python
group = await broadcast_consumer.fetch_group(
"events",
sequence=42,
options=moq.FetchGroupOptions(priority=10),
)
async for frame in group:
print(frame)
```

A retained group resolves immediately. To serve a group that is not retained, keep a dynamic handler alive on its producer:

```python
dynamic = track.dynamic()

async for request in dynamic:
group = request.accept()
group.write_frame(load_archived_frame(request.sequence))
group.finish()
```

Call `request.abort(code)` when the requested group cannot be produced. Fetch is currently a single-group operation and is supported by the moq-lite 05+ FETCH wire path.

### On-demand raw tracks

Use a dynamic broadcast when subscribers should be able to request raw tracks that are not published yet:
Expand Down
29 changes: 29 additions & 0 deletions doc/lib/swift/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,35 @@ try audio.finish()
try broadcast.finish()
```

### Fetching raw groups

Fetch retrieves one group by track name and group sequence without keeping a live subscription:

```swift
let group = try await consumer.fetchGroup(
name: "events",
sequence: 42,
options: FetchGroupOptions(priority: 10)
)
for try await frame in group {
print(frame)
}
```

A retained group resolves immediately. To serve a group that is not retained, keep a dynamic handler alive on its producer:

```swift
let dynamic = try track.dynamic()

for try await request in dynamic {
let group = try request.accept()
try group.writeFrame(loadArchivedFrame(request.sequence))
try group.finish()
}
```

Call `request.abort(errorCode:)` when the requested group cannot be produced. Fetch is currently a single-group operation and is supported by the moq-lite 05+ FETCH wire path.

### On-demand raw tracks

Use a dynamic broadcast when subscribers should be able to request raw tracks that are not published yet:
Expand Down
2 changes: 2 additions & 0 deletions go/wrapper/moq/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ var (
ErrInvalidErrorCode = ffi.ErrMoqErrorInvalidErrorCode
ErrUnauthorized = ffi.ErrMoqErrorUnauthorized
ErrForbidden = ffi.ErrMoqErrorForbidden
ErrNotFound = ffi.ErrMoqErrorNotFound
ErrUnsupported = ffi.ErrMoqErrorUnsupported
ErrLog = ffi.ErrMoqErrorLog
)

Expand Down
79 changes: 79 additions & 0 deletions go/wrapper/moq/moq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,85 @@ func TestPublishMediaLifecycle(t *testing.T) {
}
}

func TestFetchGroupAndServeDynamicMiss(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()

broadcast, err := moq.NewBroadcastProducer()
if err != nil {
t.Fatal(err)
}
track, err := broadcast.PublishTrack("events", nil)
if err != nil {
t.Fatal(err)
}
consumer, err := broadcast.Consume()
if err != nil {
t.Fatal(err)
}

cached, err := track.AppendGroup()
if err != nil {
t.Fatal(err)
}
if err := cached.WriteFrame([]byte("cached")); err != nil {
t.Fatal(err)
}
if err := cached.Finish(); err != nil {
t.Fatal(err)
}

fetched, err := consumer.FetchGroup("events", 0, &moq.FetchGroupOptions{Priority: 3})
if err != nil {
t.Fatal(err)
}
frame, err := fetched.ReadFrame(ctx)
if err != nil || string(frame) != "cached" {
t.Fatalf("cached fetch: frame=%q err=%v", frame, err)
}

dynamic, err := track.Dynamic()
if err != nil {
t.Fatal(err)
}
type fetchResult struct {
group *moq.GroupConsumer
err error
}
result := make(chan fetchResult, 1)
go func() {
group, err := consumer.FetchGroup("events", 7, &moq.FetchGroupOptions{Priority: 11})
result <- fetchResult{group: group, err: err}
}()

request, err := dynamic.RequestedGroup(ctx)
if err != nil {
t.Fatal(err)
}
if request.Sequence() != 7 || request.Priority() != 11 {
t.Fatalf("unexpected request: sequence=%d priority=%d", request.Sequence(), request.Priority())
}
produced, err := request.Accept()
if err != nil {
t.Fatal(err)
}
if err := produced.WriteFrame([]byte("archive")); err != nil {
t.Fatal(err)
}
if err := produced.Finish(); err != nil {
t.Fatal(err)
}

res := <-result
if res.err != nil {
t.Fatal(res.err)
}
frame, err = res.group.ReadFrame(ctx)
if err != nil || string(frame) != "archive" {
t.Fatalf("dynamic fetch: frame=%q err=%v", frame, err)
}
}

func TestUnknownFormat(t *testing.T) {
broadcast, err := moq.NewBroadcastProducer()
if err != nil {
Expand Down
Loading
Loading