Skip to content

d6o/GoYoutubeLounge

Repository files navigation

GoYouTubeLounge

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.

Install

go get github.com/d6o/goyoutubelounge

Requires Go 1.25 or later.

Quick Start

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
		}
	}
}

Pairing

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.

Pair with a code

client := lounge.NewClient("MyRemote")

if err := client.Pair(ctx, "A1B2"); err != nil {
    log.Fatal(err)
}

Persist and restore auth state

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)

Refresh an expired token

Lounge tokens can expire. Refresh them without re-pairing:

if err := client.RefreshAuth(ctx); err != nil {
    log.Fatal(err)
}

Check if the TV is online

available, err := client.IsAvailable(ctx)
if err != nil {
    log.Fatal(err)
}
fmt.Println("TV online:", available)

Sending Commands

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)

Receiving Events

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
    }
}

Playback states

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

Client Options

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),
)

Error Handling

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)
}

Development

Running tests

go test ./...

Regenerating mocks

Mocks are generated with uber-go/mock using Go 1.25's tool directive (no local install needed):

go generate ./...

Credits and References

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.

Disclaimer

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.

License

MIT

About

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.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages