From 2a718e4652f16aa5fae1f5f8fd07021e3f3d91a5 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Tue, 11 Aug 2026 14:46:50 +0200 Subject: [PATCH] fix: guard all R2 operations behind nil client checks for local dev support --- backend/playout/src/playlist.go | 54 +++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/backend/playout/src/playlist.go b/backend/playout/src/playlist.go index 368eb8f..cf29cb9 100644 --- a/backend/playout/src/playlist.go +++ b/backend/playout/src/playlist.go @@ -59,8 +59,21 @@ const ( manifestCacheControl = "public, max-age=1, s-maxage=2, must-revalidate" ) -// initR2Client initializes the R2 (S3-compatible) client +// isR2Configured returns true when the required R2 environment variables are set. +func isR2Configured() bool { + return os.Getenv("R2_ENDPOINT") != "" && + os.Getenv("R2_ACCESS_KEY_ID") != "" && + os.Getenv("R2_SECRET_ACCESS_KEY") != "" && + os.Getenv("R2_BUCKET_NAME") != "" +} + +// initR2Client initializes the R2 (S3-compatible) client. +// Returns an error if required environment variables are missing. func initR2Client() error { + if !isR2Configured() { + return fmt.Errorf("R2 not configured: R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, or R2_BUCKET_NAME is empty") + } + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("auto"), config.WithCredentialsProvider(aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { @@ -566,10 +579,9 @@ func main() { os.Exit(1) } - // Initialize R2 client + // Initialize R2 client (skip if not configured — local HLS via nginx still works) if err := initR2Client(); err != nil { - fmt.Println("Warning: R2 client initialization failed:", err) - // Continue without R2 for local testing + fmt.Println("R2 sync disabled:", err) } // Start HTTP health check server @@ -612,9 +624,14 @@ func main() { continue } - // Start filesystem watcher for real-time R2 segment upload (STREAMING.md) - watchCtx, watchCancel := context.WithCancel(context.Background()) - go watchAndPublishSegments(watchCtx, hlsDir) + // Start filesystem watcher for real-time R2 segment upload (STREAMING.md). + // Only active when R2 is configured; otherwise the watcher is skipped. + var watchCtx context.Context + var watchCancel context.CancelFunc + if r2Client != nil { + watchCtx, watchCancel = context.WithCancel(context.Background()) + go watchAndPublishSegments(watchCtx, hlsDir) + } fmt.Printf("[%s] Starting FFmpeg playout with browser-safe H.264/AAC HLS encoding...\n", time.Now().Format(time.RFC3339)) @@ -635,7 +652,9 @@ func main() { filepath.Join(hlsDir, "playlist.m3u8")) runErr := cmd.Run() - watchCancel() // Stop the filesystem watcher + if watchCancel != nil { + watchCancel() // Stop the filesystem watcher + } // Track restart and check for slate escalation restartCount++ @@ -659,15 +678,18 @@ func main() { deactivateSlate() } - // After FFmpeg run, publish remaining segments and atomic manifest to R2 - if err := syncLatestToR2(hlsDir); err != nil { - fmt.Println("R2 sync warning:", err) - } + // After FFmpeg run, publish remaining segments and atomic manifest to R2. + // Skipped when R2 is not configured. + if r2Client != nil { + if err := syncLatestToR2(hlsDir); err != nil { + fmt.Println("R2 sync warning:", err) + } - // Publish manifest atomically (STREAMING.md safe publication) - manifestPath := filepath.Join(hlsDir, "playlist.m3u8") - if err := publishManifest(manifestPath); err != nil { - fmt.Println("Manifest publish warning:", err) + // Publish manifest atomically (STREAMING.md safe publication) + manifestPath := filepath.Join(hlsDir, "playlist.m3u8") + if err := publishManifest(manifestPath); err != nil { + fmt.Println("Manifest publish warning:", err) + } } time.Sleep(5 * time.Second)