A Go library for controlling YouTube playback on smart TVs, Chromecast, and other connected devices via the YouTube Lounge API.
This library allows you to pair with a TV, send playback commands (play, pause, seek, volume, etc.), and receive real-time events (now playing, state changes, volume updates) through a channel-based API.
go get github.com/d6o/goyoutubeloungeRequires Go 1.25 or later.
package main
import (
"context"
"fmt"
"log"
"github.com/d6o/goyoutubelounge"
"github.com/d6o/goyoutubelounge/event"
)
func main() {
ctx := context.Background()
client := lounge.NewClient("MyRemote")
// Pair using the code shown on the TV screen
// (Settings > Link with TV code)
if err := client.Pair(ctx, "XXXX"); err != nil {
log.Fatal(err)
}
fmt.Println("Paired with:", client.ScreenName())
// Connect to the session
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
// Play a video
if err := client.PlayVideo(ctx, "dQw4w9WgXcQ"); err != nil {
log.Fatal(err)
}
// Listen for events
events, err := client.Subscribe(ctx)
if err != nil {
log.Fatal(err)
}
for ev := range events {
switch e := ev.(type) {
case *event.NowPlayingEvent:
fmt.Printf("Now playing: %s\n", e.VideoID)
case *event.PlaybackStateEvent:
fmt.Printf("State: %s at %.1fs\n", e.State, e.CurrentTime)
case *event.VolumeChangedEvent:
fmt.Printf("Volume: %d%% muted: %v\n", e.Volume, e.Muted)
case *event.DisconnectedEvent:
fmt.Printf("Disconnected: %s\n", e.Reason)
return
}
}
}Before sending commands you must pair with a TV. Open the YouTube app on your TV, go to **Settings > Link with TV code **, and use the displayed code.
client := lounge.NewClient("MyRemote")
if err := client.Pair(ctx, "A1B2"); err != nil {
log.Fatal(err)
}Save the auth state after pairing so you don't need to re-pair every time:
// Save after pairing
data := client.StoreAuthState()
bytes, _ := json.Marshal(data)
os.WriteFile("auth.json", bytes, 0600)
// Restore later
var data auth.AuthStateData
bytes, _ := os.ReadFile("auth.json")
json.Unmarshal(bytes, &data)
client := lounge.NewClient("MyRemote")
client.LoadAuthState(data)Lounge tokens can expire. Refresh them without re-pairing:
if err := client.RefreshAuth(ctx); err != nil {
log.Fatal(err)
}available, err := client.IsAvailable(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TV online:", available)All commands require an active connection (Connect must be called first). Every command accepts a context.Context
and returns an error.
client.Connect(ctx)
// Playback
client.Play(ctx)
client.Pause(ctx)
client.Next(ctx)
client.Previous(ctx)
client.SeekTo(ctx, 90.5) // seek to 1:30
// Play a specific video
client.PlayVideo(ctx, "dQw4w9WgXcQ")
// Volume (0-100)
client.SetVolume(ctx, 75)
// Playback speed (0.25 - 2.0)
client.SetPlaybackSpeed(ctx, 1.5)
// Autoplay
client.SetAutoPlayMode(ctx, event.AutoplayEnabled)
// Skip ad
client.SkipAd(ctx)
// Closed captions (empty language code to disable)
client.SetClosedCaptions(ctx, "en", "dQw4w9WgXcQ")
// D-pad navigation (for YouTube UI)
client.SendDpadCommand(ctx, command.DpadUp)
client.SendDpadCommand(ctx, command.DpadEnter)
// Request a now-playing update
client.GetNowPlaying(ctx)
// Disconnect gracefully
client.Disconnect(ctx)Subscribe returns a receive-only channel of event.Event. The channel closes when the session ends or the context is
cancelled.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
events, err := client.Subscribe(ctx)
if err != nil {
log.Fatal(err)
}
for ev := range events {
switch e := ev.(type) {
case *event.NowPlayingEvent:
fmt.Printf("Video: %s\n", e.VideoID)
if e.VideoID != "" {
fmt.Printf("Thumbnail: %s\n", e.ThumbnailURL(event.ThumbnailHigh))
}
case *event.PlaybackStateEvent:
fmt.Printf("State: %s pos: %.1f / %.1f\n", e.State, e.CurrentTime, e.Duration)
case *event.VolumeChangedEvent:
fmt.Printf("Volume: %d%% muted: %v\n", e.Volume, e.Muted)
case *event.AutoplayModeChangedEvent:
fmt.Printf("Autoplay mode: %s\n", e.Mode)
case *event.AdStateEvent:
fmt.Printf("Ad state: %s skip=%v\n", e.AdState, e.IsSkipEnabled)
case *event.AdPlayingEvent:
fmt.Printf("Ad: %s (%s)\n", e.AdTitle, e.AdVideoID)
case *event.SubtitlesTrackEvent:
fmt.Printf("Subtitles: %s (%s)\n", e.LanguageName, e.LanguageCode)
case *event.AutoplayUpNextEvent:
fmt.Printf("Up next: %s\n", e.VideoID)
case *event.PlaybackSpeedEvent:
fmt.Printf("Speed: %.2fx\n", e.PlaybackSpeed)
case *event.DisconnectedEvent:
fmt.Printf("Disconnected: %s\n", e.Reason)
return
}
}The State type represents the current player state:
| State | Value | Description |
|---|---|---|
StateStopped |
-1 | No video loaded |
StateBuffering |
0 | Loading / between videos |
StatePlaying |
1 | Playing |
StatePaused |
2 | Paused |
StateStarting |
3 | Starting playback |
StateAdvertisement |
1081 | Advertisement playing |
Configure the client with functional options:
// Custom HTTP client (e.g. with timeouts or proxy)
client := lounge.NewClient("MyRemote",
lounge.WithHTTPClient(myHTTPClient),
)
// Custom event channel buffer size (default: 64)
client := lounge.NewClient("MyRemote",
lounge.WithEventBufferSize(128),
)The library uses sentinel errors for precondition checks and typed errors for session failures:
err := client.Play(ctx)
// Precondition errors
if errors.Is(err, lounge.ErrNotPaired) { /* need to pair first */ }
if errors.Is(err, lounge.ErrNotLinked) { /* need lounge token, call RefreshAuth */ }
if errors.Is(err, lounge.ErrNotConnected) { /* need to call Connect first */ }
// Session errors (use errors.As)
var tokenErr *lounge.TokenExpiredError
if errors.As(err, &tokenErr) {
// Token expired, call RefreshAuth then reconnect
client.RefreshAuth(ctx)
client.Connect(ctx)
}go test ./...Mocks are generated with uber-go/mock using Go 1.25's tool directive (no local
install needed):
go generate ./...This library is a Go port inspired by the following projects:
- pyytlounge - The original Python implementation of the YouTube Lounge API by FabioGNR, which served as the primary reference for this Go port.
- YouTube Lounge API Wiki - Community-maintained documentation of the YouTube Lounge API protocol by enriquecidok.
- youtube-lounge-api - Earlier reverse-engineering work on the Lounge API.
The YouTube Lounge API is an undocumented, internal Google API. It is not officially supported and may change or break at any time without notice. This library is provided as-is for educational and personal use.
MIT