AtlatestRepositorysigil-youtube
1;;; (youtube live) - YouTube Live Streaming API.
2;;;
3;;; Manages the live broadcast lifecycle: create broadcasts and streams,
4;;; bind them together, and transition through states
5;;; (testing -> live -> complete).
6;;;
7;;; Broadcast lifecycle:
8;;; created -> ready -> testStarting -> testing -> liveStarting -> live -> complete
9
10(define-library (youtube live)
11 (import (sigil core)
12 (sigil dict)
13 (sigil string)
14 (sigil struct)
15 (sigil json)
16 (only (sigil http) build-query-string)
17 (sigil http client)
18 (youtube))
20 (export ;; Records
21 youtube-broadcast
22 youtube-broadcast?
23 youtube-broadcast-id
24 youtube-broadcast-title
25 youtube-broadcast-scheduled-start
26 youtube-broadcast-lifecycle-status
27 youtube-broadcast-stream-id
29 youtube-stream
30 youtube-stream?
31 youtube-stream-id
32 youtube-stream-title
33 youtube-stream-rtmp-url
34 youtube-stream-stream-key
36 ;; Parsing
37 parse-broadcast
38 parse-stream
40 ;; API functions
41 youtube-create-broadcast
42 youtube-create-stream
43 youtube-bind-broadcast
44 youtube-transition-broadcast
45 youtube-list-broadcasts
46 youtube-list-streams)
48 (begin
50 ;; ---------------------------------------------------------------
51 ;; Records
52 ;; ---------------------------------------------------------------
54 (define-struct youtube-broadcast
55 (id)
56 (title default: "")
57 (scheduled-start default: #f)
58 (lifecycle-status default: "created")
59 (stream-id default: #f))
61 (define-struct youtube-stream
62 (id)
63 (title default: "")
64 (rtmp-url default: #f)
65 (stream-key default: #f))
67 ;; ---------------------------------------------------------------
68 ;; Parsing
69 ;; ---------------------------------------------------------------
71 ;;; Parse a liveBroadcast resource into a youtube-broadcast record.
72 (define (parse-broadcast data)
73 (let ((snippet (dict-ref data snippet: #{}))
74 (status (dict-ref data status: #{}))
75 (content (dict-ref data contentDetails: #{})))
76 (youtube-broadcast
77 id: (dict-ref data id:)
78 title: (dict-ref snippet title: "")
79 scheduled-start: (dict-ref snippet scheduledStartTime: #f)
80 lifecycle-status: (dict-ref status lifeCycleStatus: "created")
81 stream-id: (dict-ref content boundStreamId: #f))))
83 ;;; Parse a liveStream resource into a youtube-stream record.
84 (define (parse-stream data)
85 (let* ((snippet (dict-ref data snippet: #{}))
86 (cdn (dict-ref data cdn: #{}))
87 (ingestion (dict-ref cdn ingestionInfo: #{})))
88 (youtube-stream
89 id: (dict-ref data id:)
90 title: (dict-ref snippet title: "")
91 rtmp-url: (dict-ref ingestion ingestionAddress: #f)
92 stream-key: (dict-ref ingestion streamName: #f))))
94 ;; ---------------------------------------------------------------
95 ;; API functions
96 ;; ---------------------------------------------------------------
98 ;;; Create a live broadcast (scheduled event).
99 ;;;
100 ;;; title: broadcast title
101 ;;; scheduled-start: ISO 8601 datetime (e.g., "2026-03-28T18:00:00Z")
102 ;;; Optional opts dict:
103 ;;; privacy-status: "public", "private", or "unlisted" (default: "public")
104 ;;; enable-dvr: boolean (default: #t)
105 ;;; record-from-start: boolean (default: #t)
106 ;;; enable-auto-start: boolean (default: #f)
107 ;;; enable-auto-stop: boolean (default: #f)
108 ;;;
109 ;;; Quota cost: 50 units.
110 (define (youtube-create-broadcast client title scheduled-start . rest)
111 (let ((opts (if (null? rest) #{} (car rest))))
112 (let* ((privacy (dict-ref opts privacy-status: "public"))
113 (body #{ snippet:
114 #{ title: title
115 scheduledStartTime: scheduled-start }
116 contentDetails:
117 #{ enableDvr: (dict-ref opts enable-dvr: #t)
118 recordFromStart: (dict-ref opts record-from-start: #t)
119 enableAutoStart: (dict-ref opts enable-auto-start: #f)
120 enableAutoStop: (dict-ref opts enable-auto-stop: #f)
121 monitorStream:
122 #{ enableMonitorStream: #t } }
123 status:
124 #{ privacyStatus: privacy } })
125 (url (string-append
126 (youtube-api-url client "liveBroadcasts")
127 (build-query-string
128 (list (cons "part" "snippet,contentDetails,status")))))
129 (result (youtube-post/json client url body)))
130 (parse-broadcast result))))
132 ;;; Create a live stream (ingest point).
133 ;;;
134 ;;; title: stream title
135 ;;; Optional opts dict:
136 ;;; frame-rate: "30fps", "60fps" (default: "30fps")
137 ;;; resolution: "1080p", "720p", "480p", "360p", "240p" (default: "1080p")
138 ;;; ingestion-type: "rtmp" or "dash" (default: "rtmp")
139 ;;;
140 ;;; Quota cost: 50 units.
141 (define (youtube-create-stream client title . rest)
142 (let ((opts (if (null? rest) #{} (car rest))))
143 (let* ((body #{ snippet:
144 #{ title: title }
145 cdn:
146 #{ frameRate: (dict-ref opts frame-rate: "30fps")
147 ingestionType: (dict-ref opts ingestion-type: "rtmp")
148 resolution: (dict-ref opts resolution: "1080p") } })
149 (url (string-append
150 (youtube-api-url client "liveStreams")
151 (build-query-string
152 (list (cons "part" "snippet,cdn")))))
153 (result (youtube-post/json client url body)))
154 (parse-stream result))))
156 ;;; Bind a broadcast to a stream.
157 ;;; After binding, the stream's RTMP feed will be used for the broadcast.
158 ;;; A broadcast binds to exactly one stream; a stream can serve multiple broadcasts.
159 ;;;
160 ;;; Quota cost: 50 units.
161 (define (youtube-bind-broadcast client broadcast-id stream-id)
162 (let* ((url (string-append
163 (youtube-api-url client "liveBroadcasts" "bind")
164 (build-query-string
165 (list (cons "id" broadcast-id)
166 (cons "part" "id,contentDetails")
167 (cons "streamId" stream-id)))))
168 (result (youtube-post/json client url #{})))
169 (parse-broadcast result)))
171 ;;; Transition a broadcast to a new status.
172 ;;; Valid transitions:
173 ;;; "testing" — start preview (stream must be active, monitor stream enabled)
174 ;;; "live" — go live (stream must be active)
175 ;;; "complete" — end broadcast
176 ;;;
177 ;;; Note: stuck intermediate states (testStarting, liveStarting) require
178 ;;; deleting and recreating the broadcast.
179 ;;;
180 ;;; Quota cost: 50 units per transition.
181 (define (youtube-transition-broadcast client broadcast-id status)
182 (let* ((url (string-append
183 (youtube-api-url client "liveBroadcasts" "transition")
184 (build-query-string
185 (list (cons "id" broadcast-id)
186 (cons "broadcastStatus" status)
187 (cons "part" "status")))))
188 (result (youtube-post/json client url #{})))
189 (parse-broadcast result)))
191 ;;; List live broadcasts for the authenticated user.
192 ;;; Optional opts dict:
193 ;;; broadcast-status: "upcoming", "active", "completed", "all" (default: "all")
194 ;;; max-results: number (default: 25)
195 ;;; page-token: pagination token
196 ;;;
197 ;;; Quota cost: 1 unit.
198 (define (youtube-list-broadcasts client . rest)
199 (let ((opts (if (null? rest) #{} (car rest))))
200 (let* ((params (list
201 (cons "part" "snippet,contentDetails,status")
202 (cons "mine" "true")
203 (cons "broadcastStatus"
204 (dict-ref opts broadcast-status: "all"))
205 (cons "maxResults"
206 (number->string
207 (dict-ref opts max-results: 25)))
208 (cons "pageToken"
209 (dict-ref opts page-token: #f))))
210 (url (string-append
211 (youtube-api-url client "liveBroadcasts")
212 (build-query-string params)))
213 (data (youtube-get/json client url))
214 (items (dict-ref data items: #[])))
215 (array->list (array-map parse-broadcast items)))))
217 ;;; List live streams for the authenticated user.
218 ;;; Quota cost: 1 unit.
219 (define (youtube-list-streams client . rest)
220 (let ((opts (if (null? rest) #{} (car rest))))
221 (let* ((params (list
222 (cons "part" "snippet,cdn,status")
223 (cons "mine" "true")
224 (cons "maxResults"
225 (number->string
226 (dict-ref opts max-results: 25)))
227 (cons "pageToken"
228 (dict-ref opts page-token: #f))))
229 (url (string-append
230 (youtube-api-url client "liveStreams")
231 (build-query-string params)))
232 (data (youtube-get/json client url))
233 (items (dict-ref data items: #[])))
234 (array->list (array-map parse-stream items)))))
236 ))