git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/bunny/stream.go (about) 1 package bunny 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "strconv" 12 "time" 13 ) 14 15 type VideoStatus int64 16 17 const ( 18 VideoStatusCreated = iota 19 VideoStatusUploaded 20 VideoStatusProcessing 21 VideoStatusTranscoding 22 // Video is ready 23 VideoStatusFinished 24 VideoStatusError 25 VideoStatusUploadFailed 26 ) 27 28 type FetchVideoInput struct { 29 Url string `json:"url"` 30 } 31 32 type FetchVideoOutput struct { 33 Success bool `json:"success"` 34 Message string `json:"message"` 35 StatusCode int64 `json:"statusCode"` 36 37 ID string `json:"id"` 38 } 39 40 type UploadVideoOutput struct { 41 Success bool `json:"success"` 42 Message string `json:"message"` 43 StatusCode int64 `json:"statusCode"` 44 } 45 46 // See https://docs.bunny.net/reference/video_getvideo for details 47 type Video struct { 48 VideoLibraryID int64 `json:"videoLibraryId"` 49 Guid string `json:"guid"` 50 Title string `json:"title"` 51 // "dateUploaded": "2023-08-17T12:45:11.381", 52 Views int64 `json:"views"` 53 IsPublic bool `json:"isPublic"` 54 // Length is the duration of the video in seconds 55 Length int64 `json:"length"` 56 Status VideoStatus `json:"status"` 57 Framerate float64 `json:"framerate"` 58 // "rotation": 0, 59 Width int64 `json:"width"` 60 Height int64 `json:"height"` 61 // The available resolutions of the video. "360p,720p,1080p" for example 62 AvailableResolutions string `json:"availableResolutions"` 63 ThumbnailCount int64 `json:"thumbnailCount"` 64 // The current encode progress of the video 65 EncodeProgress int64 `json:"encodeProgress"` 66 // The amount of storage used by this video 67 StorageSize int64 `json:"storageSize"` 68 // "captions": [], 69 HasMP4Fallback bool `json:"hasMP4Fallback"` 70 // The ID of the collection where the video belongs. Can be empty. 71 CollectionID string `json:"collectionId"` 72 // The file name of the thumbnail inside of the storage 73 ThumbnailFileName string `json:"thumbnailFileName"` 74 // The average watch time of the video in seconds 75 AverageWatchTime int64 `json:"averageWatchTime"` 76 // The total video watch time in seconds 77 TotalWatchTime int64 `json:"totalWatchTime"` 78 // The automatically detected category of the video. Default: "unknown" 79 Category string `json:"category"` 80 Chapters []VideoChapter `json:"chapters"` 81 Moments []VideoMoment `json:"moments"` 82 MetaTags []VideoMetaTag `json:"metaTags"` 83 // "transcodingMessages": [] 84 } 85 86 type VideoChapter struct { 87 Title string `json:"title"` 88 Start int64 `json:"start"` 89 End int64 `json:"end"` 90 } 91 92 type VideoMoment struct { 93 Label string `json:"label"` 94 Timestamp int64 `json:"timestamp"` 95 } 96 97 type VideoMetaTag struct { 98 Property string `json:"property"` 99 Value string `json:"value"` 100 } 101 102 type UpdateVideoInput struct { 103 LibraryID string `json:"-"` 104 VideoID string `json:"-"` 105 Title *string `json:"title,omitempty"` 106 CollectionID *string `json:"collectionId,omitempty"` 107 Chapters []VideoChapter `json:"chapters,omitempty"` 108 Moments []VideoMoment `json:"moments,omitempty"` 109 MetaTags []VideoMetaTag `json:"metaTags,omitempty"` 110 } 111 112 type CreateVideoInput struct { 113 LibraryID string `json:"-"` 114 Title string `json:"title"` 115 CollectionID *string `json:"collectionId,omitempty"` 116 ThumbnailTime *int64 `json:"thumbnailTime,omitempty"` 117 } 118 119 type UploadVideoInput struct { 120 LibraryID string 121 VideoID string 122 Data io.Reader 123 } 124 125 // See https://docs.bunny.net/docs/stream-embed-token-authentication 126 func (client *Client) SignVideoUrl(videoId string, expiresAt time.Time) (token, expiresAtStr string) { 127 expiresAtStr = strconv.FormatInt(expiresAt.Unix(), 10) 128 129 data := append([]byte(client.streamApiKey), []byte(videoId)...) 130 data = append(data, []byte(expiresAtStr)...) 131 signature := sha256.Sum256(data) 132 133 token = hex.EncodeToString(signature[:]) 134 return 135 } 136 137 // See here for the avilable options: https://docs.bunny.net/docs/stream-embedding-videos#parameters 138 type GenerateIframeVideoUrlOptions struct { 139 Signed bool 140 // If Signed is true and Expires is nil we use a default value of 24 hours 141 Expires *time.Time 142 Autoplay *bool 143 // We recommend to set to false 144 TrackView *bool 145 // We recommend to set to true 146 Preload *bool 147 Captions *string 148 VideoStartTime *uint64 149 Refresh *bool 150 } 151 152 // GenerateIframeVideoUrl generates the URL of the given video to be embeded with an iframe 153 // See https://docs.bunny.net/docs/stream-embedding-videos for more information 154 // See https://docs.bunny.net/docs/stream-embed-token-authentication for documentation about authentication 155 func (client *Client) GenerateIframeVideoUrl(libraryID string, videoID string, options *GenerateIframeVideoUrlOptions) (videoUrl string) { 156 var queryValue string 157 videoUrl = fmt.Sprintf("https://iframe.mediadelivery.net/embed/%s/%s", libraryID, videoID) 158 159 if options != nil { 160 queryParms := url.Values{} 161 162 if options.TrackView != nil { 163 queryParms.Set("trackView", strconv.FormatBool(*options.TrackView)) 164 } 165 if options.Refresh != nil { 166 queryParms.Set("refresh", strconv.FormatBool(*options.Refresh)) 167 } 168 if options.Autoplay != nil { 169 queryParms.Set("autoplay", strconv.FormatBool(*options.Autoplay)) 170 } 171 if options.Preload != nil { 172 queryParms.Set("preload", strconv.FormatBool(*options.Preload)) 173 } 174 if options.Captions != nil { 175 queryParms.Set("captions", *options.Captions) 176 } 177 if options.VideoStartTime != nil { 178 queryParms.Set("t", strconv.FormatUint(*options.VideoStartTime, 10)) 179 } 180 181 if options.Signed { 182 var expiresAt time.Time 183 if options.Expires != nil { 184 expiresAt = *options.Expires 185 } else { 186 expiresAt = time.Now().UTC().Add(24 * time.Hour) 187 } 188 token, expiresAtStr := client.SignVideoUrl(videoID, expiresAt) 189 queryParms.Set("token", token) 190 queryParms.Set("expires", expiresAtStr) 191 } 192 193 queryValue = queryParms.Encode() 194 } 195 196 if queryValue != "" { 197 videoUrl += "?" + queryValue 198 } 199 200 return 201 } 202 203 // https://docs.bunny.net/reference/video_fetchnewvideo 204 func (client *Client) FetchVideo(ctx context.Context, libraryID, videoUrl string) (output FetchVideoOutput, err error) { 205 err = client.request(ctx, requestParams{ 206 Payload: FetchVideoInput{ 207 Url: videoUrl, 208 }, 209 Method: http.MethodPost, 210 URL: fmt.Sprintf("%s/library/%s/videos/fetch", client.streamApiBaseUrl, libraryID), 211 useStreamApiKey: true, 212 }, &output) 213 214 return 215 } 216 217 // https://docs.bunny.net/reference/video_deletevideo 218 func (client *Client) DeleteVideo(ctx context.Context, libraryID, videoID string) (err error) { 219 err = client.request(ctx, requestParams{ 220 Payload: nil, 221 Method: http.MethodDelete, 222 URL: fmt.Sprintf("%s/library/%s/videos/%s", client.streamApiBaseUrl, libraryID, videoID), 223 useStreamApiKey: true, 224 }, nil) 225 226 return 227 } 228 229 // https://docs.bunny.net/reference/video_reencodevideo 230 func (client *Client) ReencodeVideo(ctx context.Context, libraryID, videoID string) (video Video, err error) { 231 err = client.request(ctx, requestParams{ 232 Payload: nil, 233 Method: http.MethodPost, 234 URL: fmt.Sprintf("%s/library/%s/videos/%s/reencode", client.streamApiBaseUrl, libraryID, videoID), 235 useStreamApiKey: true, 236 }, &video) 237 238 return 239 } 240 241 // https://docs.bunny.net/reference/video_getvideo 242 func (client *Client) GetVideo(ctx context.Context, libraryID, videoID string) (video Video, err error) { 243 err = client.request(ctx, requestParams{ 244 Payload: nil, 245 Method: http.MethodGet, 246 URL: fmt.Sprintf("%s/library/%s/videos/%s", client.streamApiBaseUrl, libraryID, videoID), 247 useStreamApiKey: true, 248 }, &video) 249 250 return 251 } 252 253 // https://docs.bunny.net/reference/video_updatevideo 254 func (client *Client) UpdateVideo(ctx context.Context, input UpdateVideoInput) (err error) { 255 err = client.request(ctx, requestParams{ 256 Payload: nil, 257 Method: http.MethodPost, 258 URL: fmt.Sprintf("%s/library/%s/videos/%s", client.streamApiBaseUrl, input.LibraryID, input.VideoID), 259 useStreamApiKey: true, 260 }, nil) 261 262 return 263 } 264 265 // https://docs.bunny.net/reference/video_createvideo 266 func (client *Client) CreateVideo(ctx context.Context, input CreateVideoInput) (output Video, err error) { 267 err = client.request(ctx, requestParams{ 268 Payload: input, 269 Method: http.MethodPost, 270 URL: fmt.Sprintf("%s/library/%s/videos", client.streamApiBaseUrl, input.LibraryID), 271 useStreamApiKey: true, 272 }, &output) 273 274 return 275 } 276 277 // https://docs.bunny.net/reference/video_createvideo 278 func (client *Client) UploadVideo(ctx context.Context, input UploadVideoInput) (output UploadVideoOutput, err error) { 279 err = client.upload(ctx, uploadParams{ 280 Data: input.Data, 281 Method: http.MethodPut, 282 URL: fmt.Sprintf("%s/library/%s/videos/%s", client.streamApiBaseUrl, input.LibraryID, input.VideoID), 283 useStreamApiKey: true, 284 }, &output) 285 286 return 287 }