AtlatestRepositorysigil-youtube
sigil-youtube / tree / src / youtubelive.sgl
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 states5
;;; (testing -> live -> complete).6
;;;7
;;; Broadcast lifecycle:8
;;; created -> ready -> testStarting -> testing -> liveStarting -> live -> complete10
(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 ;; Records21
youtube-broadcast22
youtube-broadcast?23
youtube-broadcast-id24
youtube-broadcast-title25
youtube-broadcast-scheduled-start26
youtube-broadcast-lifecycle-status27
youtube-broadcast-stream-id29
youtube-stream30
youtube-stream?31
youtube-stream-id32
youtube-stream-title33
youtube-stream-rtmp-url34
youtube-stream-stream-key36
;; Parsing37
parse-broadcast38
parse-stream40
;; API functions41
youtube-create-broadcast42
youtube-create-stream43
youtube-bind-broadcast44
youtube-transition-broadcast45
youtube-list-broadcasts46
youtube-list-streams)48
(begin50
;; ---------------------------------------------------------------51
;; Records52
;; ---------------------------------------------------------------54
(define-struct youtube-broadcast55
(id)56
(title default: "")57
(scheduled-start default: #f)58
(lifecycle-status default: "created")59
(stream-id default: #f))61
(define-struct youtube-stream62
(id)63
(title default: "")64
(rtmp-url default: #f)65
(stream-key default: #f))67
;; ---------------------------------------------------------------68
;; Parsing69
;; ---------------------------------------------------------------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-broadcast77
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-stream89
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 functions96
;; ---------------------------------------------------------------98
;;; Create a live broadcast (scheduled event).99
;;;100
;;; title: broadcast title101
;;; 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: title115
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-append126
(youtube-api-url client "liveBroadcasts")127
(build-query-string128
(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 title135
;;; 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-append150
(youtube-api-url client "liveStreams")151
(build-query-string152
(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-append163
(youtube-api-url client "liveBroadcasts" "bind")164
(build-query-string165
(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 broadcast176
;;;177
;;; Note: stuck intermediate states (testStarting, liveStarting) require178
;;; 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-append183
(youtube-api-url client "liveBroadcasts" "transition")184
(build-query-string185
(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 token196
;;;197
;;; Quota cost: 1 unit.198
(define (youtube-list-broadcasts client . rest)199
(let ((opts (if (null? rest) #{} (car rest))))200
(let* ((params (list201
(cons "part" "snippet,contentDetails,status")202
(cons "mine" "true")203
(cons "broadcastStatus"204
(dict-ref opts broadcast-status: "all"))205
(cons "maxResults"206
(number->string207
(dict-ref opts max-results: 25)))208
(cons "pageToken"209
(dict-ref opts page-token: #f))))210
(url (string-append211
(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 (list222
(cons "part" "snippet,cdn,status")223
(cons "mine" "true")224
(cons "maxResults"225
(number->string226
(dict-ref opts max-results: 25)))227
(cons "pageToken"228
(dict-ref opts page-token: #f))))229
(url (string-append230
(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
))