-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
213 lines (185 loc) · 7.13 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"sort"
"strings"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humago"
"github.com/danielgtaylor/huma/v2/conditional"
_ "embed"
)
//go:embed README.md
var apiDesc string
type PublishPoint struct {
ID string `json:"id" example:"pub1" doc:"The unique identifier for the publish point."`
Format string `json:"format" enum:"hls,dash" doc:"The format to publish in."`
URL string `json:"url" format:"uri" doc:"The URL to publish to."`
DRMs []string `json:"drms,omitempty" enum:"fairplay,widevine,playready" doc:"A list of DRM systems to use."`
Headers map[string]string `json:"headers,omitempty" doc:"A map of headers to include in the request."`
}
type VideoEncoder struct {
ID string `json:"id" example:"hd1" doc:"The unique identifier for the encoder."`
Width uint32 `json:"width" multipleOf:"2" example:"1920" doc:"The width of the video in pixels. Width & height must result in an aspect ratio of 16:9."`
Height uint32 `json:"height" multipleOf:"2" example:"1080" doc:"The height of the video in pixels. Width & height must result in an aspect ratio of 16:9."`
Bitrate uint16 `json:"bitrate" minimum:"300" doc:"The target bitrate for the video in kbps."`
Framerate float64 `json:"framerate" enum:"30,25,29.97,50,60" doc:"The target framerate for the video in frames per second."`
}
func (v *VideoEncoder) Resolve(ctx huma.Context, prefix *huma.PathBuffer) []error {
// Custom validation for the aspect ratio.
if float64(v.Width)/float64(v.Height) != 16.0/9.0 {
return []error{&huma.ErrorDetail{
Message: "width and height must be in a 16:9 (1.777) aspect ratio",
Location: prefix.String(),
Value: float64(v.Width) / float64(v.Height),
}}
}
return nil
}
type Channel struct {
Name string `json:"name" maxLength:"80" doc:"The friendly name of the channel."`
Region string `json:"region" enum:"us-west,us-east" doc:"The desired region to run the channel in."`
On bool `json:"on,omitempty" doc:"Whether the channel is currently running."`
SegmentDuration uint8 `json:"segment_duration" minimum:"2" maximum:"60" doc:"The duration of each video segment in seconds."`
Tags []string `json:"tags,omitempty" maxItems:"10" example:"[\"event\", \"olympics\"]" doc:"A list of tags for the channel."`
VideoEncoders []VideoEncoder `json:"video_encoders" minItems:"1" doc:"A list of video encoder settings use."`
PublishPoints []PublishPoint `json:"publish_points,omitempty" doc:"A list of publishing points to use."`
}
// ChannelMeta is used both as the DB storage object as well as the response
// for listing channels.
type ChannelMeta struct {
ID string `json:"id" doc:"Channel ID"`
ETag string `json:"etag" doc:"The content hash for the channel"`
LastModified time.Time `json:"last_modified" doc:"The last modified time for the channel"`
Channel *Channel `json:"-"`
}
// ChannelIDParam is a shared input path parameter used by several operations.
type ChannelIDParam struct {
ChannelID string `path:"id" pattern:"[a-zA-Z0-9_-]{2,60}" doc:"The unique identifier of the channel."`
}
type ListChannelsResponse struct {
Link string `header:"Link" doc:"Links for pagination"`
Body []*ChannelMeta
}
type GetChannelResponse struct {
ETag string `header:"Etag" doc:"The content hash for the channel"`
LastModified time.Time `header:"Last-Modified" doc:"The last modified time for the channel"`
Body *Channel
}
type PutChannelResponse struct {
ETag string `header:"ETag" doc:"The content hash for the channel"`
}
// setup our API middleware, operations, and handlers.
func setup(api huma.API, db DB[*ChannelMeta]) {
// Middleware example to log requests.
api.UseMiddleware(func(ctx huma.Context, next func(huma.Context)) {
// Basic tracing support.
traceID := GetTraceID()
ctx = huma.WithValue(ctx, ctxKeyTraceID, traceID)
ctx.SetHeader("traceparent", traceID)
next(ctx)
// Log the request.
slog.Info("Request",
"method", ctx.Method(),
"path", ctx.URL().Path,
"status", ctx.Status(),
"trace_id", traceID,
)
})
huma.Get(api, "/channels", func(ctx context.Context, input *struct {
Cursor string `query:"cursor" doc:"The cursor to use for pagination."`
}) (*ListChannelsResponse, error) {
// TODO: pagination!
metas := []*ChannelMeta{}
db.Range(func(key string, value *ChannelMeta) bool {
metas = append(metas, value)
return true
})
sort.Slice(metas, func(i, j int) bool {
// Bit of a hack due to the in-memory map, but let's make sure to send
// clients a stable order of channels.
return metas[i].LastModified.After(metas[j].LastModified)
})
return &ListChannelsResponse{
Body: metas,
}, nil
})
huma.Get(api, "/channels/{id}", func(ctx context.Context, input *struct {
ChannelIDParam
}) (*GetChannelResponse, error) {
meta, ok := db.Load(input.ChannelID)
if !ok {
return nil, huma.Error404NotFound("Channel not found")
}
return &GetChannelResponse{
ETag: meta.ETag,
LastModified: meta.LastModified,
Body: meta.Channel,
}, nil
})
huma.Put(api, "/channels/{id}", func(ctx context.Context, input *struct {
ChannelIDParam
conditional.Params
Body *Channel
}) (*PutChannelResponse, error) {
etag := ""
modified := time.Time{}
existing, ok := db.Load(input.ChannelID)
if ok {
etag = existing.ETag
modified = existing.LastModified
}
if input.HasConditionalParams() {
// Conditional update, so fail if the ETag/modified time doesn't match.
// This prevents multiple distributed clients from overwriting each other.
if err := input.PreconditionFailed(etag, modified); err != nil {
return nil, err
}
}
if existing != nil && Hash(input.Body) == Hash(existing.Channel) {
return nil, huma.Status304NotModified()
}
meta := &ChannelMeta{
ID: input.ChannelID,
ETag: Hash(input.Body),
LastModified: time.Now(),
Channel: input.Body,
}
db.Store(input.ChannelID, meta)
return &PutChannelResponse{
ETag: meta.ETag,
}, nil
})
huma.Delete(api, "/channels/{id}", func(ctx context.Context, input *struct {
ChannelIDParam
}) (*struct{}, error) {
db.Delete(input.ChannelID)
return nil, nil
})
}
func main() {
// Create a new router & API
router := http.NewServeMux()
// Set up and create the API with some basic info.
config := huma.DefaultConfig("Channel API Demo", "1.0.0")
config.OpenAPI.Info.Description = strings.TrimPrefix(apiDesc, "# Huma Demo")
config.OpenAPI.Info.Contact = &huma.Contact{
Name: "Channel API Support",
Email: "[email protected]",
}
api := humago.New(router, config)
// Initialize the DB. This is a goroutine-safe in-memory map for the demo,
// but would be a real data store in a production system.
db := NewDB[*ChannelMeta]("channels.db")
// Register all our API operations & handlers.
setup(api, db)
// Run the server!
fmt.Println("Listening on http://localhost:8888")
err := http.ListenAndServe("localhost:8888", router)
if err != http.ErrServerClosed {
panic(err)
}
}