Chromecast IntegrationΒΆ
Mixtape Society supports casting entire mixtapes to Chromecast devices, enabling users to play their curated music collections on TVs and speakers throughout their home with seamless queue management and unified controls.
π― OverviewΒΆ
Chromecast integration provides:
- Full mixtape casting - Load entire playlist as a queue
- Unified controls - Control Chromecast from phone, computer, or lock screen
- Media Session sync - Lock screen and notification controls mirror Chromecast state
- Local player silencing - Prevents duplicate media controls and battery drain
- Quality-aware streaming - Respects quality parameter for bandwidth management
- Cover art display - Shows mixtape and track artwork on TV/receiver
ποΈ ArchitectureΒΆ
System ComponentsΒΆ
graph TB
User[User Interface]
CastBtn[Cast Button]
ChromecastJS[chromecast.js]
PlayerControls[playerControls.js]
PlayerUtils[playerUtils.js]
CastSDK[Google Cast SDK]
MediaSession[Media Session API]
Server[Flask Server]
Receiver[Chromecast Device]
User --> CastBtn
CastBtn --> ChromecastJS
ChromecastJS --> CastSDK
ChromecastJS --> PlayerUtils
ChromecastJS --> MediaSession
PlayerControls --> ChromecastJS
CastSDK <--> Receiver
Receiver --> Server
style ChromecastJS fill:#4a6fa5
style CastSDK fill:#c65d5d
style Receiver fill:#4a8c5f
Data FlowΒΆ
sequenceDiagram
actor User
participant Browser as Browser Player
participant ChromecastJS as chromecast.js
participant CastSDK as Google Cast SDK
participant Receiver as Chromecast Device
participant Server as Flask Server
User->>Browser: Click cast button
Browser->>ChromecastJS: extractTracksFromDOM()
ChromecastJS-->>Browser: tracks array
Browser->>ChromecastJS: loadPlaylistAndCast(tracks, startIndex)
alt Existing cast session
ChromecastJS->>CastSDK: loadQueue(currentSession)
else No active session
ChromecastJS->>CastSDK: requestSession()
CastSDK-->>ChromecastJS: new session
ChromecastJS->>CastSDK: loadQueue(session)
end
ChromecastJS->>ChromecastJS: silenceLocalPlayer()
ChromecastJS->>ChromecastJS: clearMediaSession()
CastSDK->>Receiver: QueueLoadRequest with tracks
loop For each track
Receiver->>Server: GET /play/<path>?quality=medium
Server-->>Receiver: 206/200 audio stream + CORS headers
Receiver->>Receiver: Buffer & decode
end
Receiver->>CastSDK: Media state updates
CastSDK->>ChromecastJS: Player state callbacks
ChromecastJS->>Browser: Update UI state
Note over Browser,Receiver: User controls playback via phone/computer
Note over Receiver: Audio plays on Chromecast device
π¦ Core ModulesΒΆ
1. chromecast.jsΒΆ
Location: static/js/player/chromecast.js
Main module handling all Chromecast interactions.
InitializationΒΆ
Responsibilities:
- Loads Google Cast SDK from CDN
- Waits for
__onGCastApiAvailablecallback - Calls
initializeCastApi()when ready - Dispatches
cast:readyevent for UI components
Implementation:
function initializeCast() {
if (typeof chrome === 'undefined' || !chrome.cast) {
console.warn('Chrome Cast API not available');
return;
}
window['__onGCastApiAvailable'] = function(isAvailable) {
if (isAvailable) {
initializeCastApi();
}
};
// Load Cast SDK
const script = document.createElement('script');
script.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
document.head.appendChild(script);
}
Cast API ConfigurationΒΆ
Configuration:
- Application ID:
CC1AD845(Default Media Receiver) - Auto Join Policy:
TAB_AND_ORIGIN_SCOPED - Language: User's browser language
- Resume Saved Session:
true
Session Listeners:
sessionListener()- Handles new cast sessionsreceiverListener()- Monitors available cast devices
Session ManagementΒΆ
Starting a Session:
Process:
- Check for existing session
- Request new session if none active
- Build queue items with metadata for each track
- Create
QueueLoadRequestwith all tracks - Load queue to Chromecast (triggers automatic track detection)
- Silence local player
- Update UI state
Queue Item Structure:
const queueItem = new chrome.cast.media.QueueItem(mediaInfo);
queueItem.itemId = index; // Critical for track change detection
Stopping a Session:
Process:
- Stop current Chromecast media
- End cast session
- Restore local player controls
- Clear Media Session
- Reset UI state
Media Control FunctionsΒΆ
export function castPlay()
export function castPause()
export function castNext()
export function castPrevious()
export function castSeek(time)
export function castJumpToTrack(index)
export function castTogglePlayPause()
Features:
- Direct Chromecast control
- Error handling
- State validation
- Callback notifications
State ManagementΒΆ
Global State Variables:
export let globalCastingState = false; // Is casting active?
let currentCastSession = null; // Current Cast session
let currentMedia = null; // Current media controller
let castPlayState = 'IDLE'; // Current play state
State Change Callbacks:
let castControlCallbacks = {
onCastStart: null,
onCastEnd: null,
onTrackChange: null,
onPlayStateChange: null,
onTimeUpdate: null,
onVolumeChange: null
};
export function setCastControlCallbacks(callbacks)
Callback Purposes:
onCastStart- Called when cast session beginsonCastEnd- Called when cast session endsonTrackChange- Called when track changes (via queue events)onPlayStateChange- Called when play/pause state changesonTimeUpdate- Called every second with playback positiononVolumeChange- Called when Chromecast volume changes
Track Change DetectionΒΆ
Track changes are detected via the Cast SDK's queue events:
remotePlayerController.addEventListener(
cast.framework.RemotePlayerEventType.CURRENT_ITEM_CHANGED,
handleTrackChange
);
Detection Process:
CURRENT_ITEM_CHANGEDevent fires when track changes- Get current
itemIdfrom media session - Map
itemIdto track index in playlist - Call
onTrackChange(index)callback - UI updates to show new track
Works with:
- Next/Previous buttons in your UI
- Google Home app controls
- Chromecast device controls
- Voice commands ("Hey Google, next song")
2. Media Session IntegrationΒΆ
When casting is active, chromecast.js synchronizes the browser's Media Session API to mirror Chromecast state.
Purpose:
- Lock screen controls
- Notification media controls
- Hardware media keys
- Consistent UI across platforms
Implementation:
function updateMediaSessionForCast(media) {
if (!('mediaSession' in navigator)) return;
const mediaInfo = media.media;
const metadata = mediaInfo.metadata;
navigator.mediaSession.metadata = new MediaMetadata({
title: metadata.title,
artist: metadata.artist,
album: metadata.albumName,
artwork: metadata.images
});
// Route all actions to Chromecast
navigator.mediaSession.setActionHandler('play', () => castPlay());
navigator.mediaSession.setActionHandler('pause', () => castPause());
navigator.mediaSession.setActionHandler('previoustrack', () => castPrevious());
navigator.mediaSession.setActionHandler('nexttrack', () => castNext());
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (details.seekTime !== undefined) {
castSeek(details.seekTime);
}
});
// Update playback state
const isPaused = media.playerState === chrome.cast.media.PlayerState.PAUSED;
navigator.mediaSession.playbackState = isPaused ? 'paused' : 'playing';
}
3. Player Controls IntegrationΒΆ
Location: static/js/player/playerControls.js
The player controls module checks globalCastingState to route commands appropriately.
Example - Play Track:
import { globalCastingState, castJumpToTrack } from './chromecast.js';
function playTrack(index) {
if (globalCastingState) {
// Route to Chromecast
castJumpToTrack(index);
return;
}
// Local playback
const track = window.__mixtapeData.tracks[index];
const player = document.getElementById('main-player');
player.src = `/play/${track.file_path}?quality=medium`;
player.play();
}
Example - Play/Pause:
function togglePlayPause() {
if (globalCastingState) {
castTogglePlayPause();
return;
}
const player = document.getElementById('main-player');
if (player.paused) {
player.play();
} else {
player.pause();
}
}
4. Local Player ManagementΒΆ
Location: static/js/player/playerUtils.js
When casting starts, the local player must be silenced to prevent:
- Duplicate media controls
- Battery drain
- Conflicting Media Session handlers
Silencing Local Player:
export function silenceLocalPlayer() {
const player = document.getElementById('main-player');
if (!player) return;
// Pause and clear source
player.pause();
player.src = '';
player.load();
// Remove media attributes
player.removeAttribute('controls');
player.removeAttribute('autoplay');
// Mute completely
player.volume = 0;
player.muted = true;
// Remove from tab order
player.setAttribute('tabindex', '-1');
}
Restoring Local Player:
export function enableLocalPlayer() {
const player = document.getElementById('main-player');
if (!player) return;
// Restore controls
player.setAttribute('controls', '');
// Restore volume
player.volume = 1.0;
player.muted = false;
// Restore tab order
player.removeAttribute('tabindex');
}
Clearing Media Session:
export function clearMediaSession() {
if (!('mediaSession' in navigator)) return;
// Set state to 'none' FIRST (critical order)
navigator.mediaSession.playbackState = 'none';
// Clear metadata
navigator.mediaSession.metadata = null;
// Remove all action handlers
const actions = [
'play', 'pause', 'stop',
'previoustrack', 'nexttrack',
'seekbackward', 'seekforward', 'seekto'
];
actions.forEach(action => {
try {
navigator.mediaSession.setActionHandler(action, null);
} catch (e) {
// Action may not be supported
}
});
}
π Backend RequirementsΒΆ
CORS HeadersΒΆ
Chromecast devices make cross-origin requests to the Flask server and require proper CORS headers.
Implementation in routes/play.py:
@play.route("/play/<path:file_path>")
def stream_audio(file_path):
# ... validation and file resolution ...
response = send_file(serve_path, mimetype=mime_type)
# CRITICAL: CORS headers for Chromecast
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Expose-Headers"] = "Content-Type, Accept-Encoding, Range"
return response
Why these headers matter:
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin: * |
Allows Chromecast devices to fetch audio from your server |
Access-Control-Expose-Headers |
Exposes headers needed for range requests and seeking |
Range Request SupportΒΆ
Chromecast uses HTTP range requests for seeking and buffering.
Implementation:
def _handle_range_request(file_path, mime_type):
"""Handle HTTP Range requests for seeking"""
range_header = request.headers.get("Range")
if not range_header:
return None
size = os.path.getsize(file_path)
match = re.match(r"bytes=(\d+)-(\d*)", range_header)
if not match:
return abort(416) # Range Not Satisfiable
start = int(match.group(1))
end = int(match.group(2)) if match.group(2) else size - 1
# Validate range
if start >= size or end >= size or start > end:
return abort(416)
length = end - start + 1
def generate():
with open(file_path, "rb") as f:
f.seek(start)
remaining = length
while remaining > 0:
chunk_size = min(8192, remaining)
chunk = f.read(chunk_size)
if not chunk:
break
remaining -= len(chunk)
yield chunk
response = Response(
generate(),
206, # Partial Content
mimetype=mime_type,
direct_passthrough=True
)
response.headers["Content-Range"] = f"bytes {start}-{end}/{size}"
response.headers["Content-Length"] = str(length)
response.headers["Accept-Ranges"] = "bytes"
# CORS headers
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Expose-Headers"] = "Content-Range, Content-Length, Range"
return response
Quality Parameter SupportΒΆ
Chromecast respects the quality query parameter for bandwidth management.
Example request:
Server behavior:
- Check if transcoded version exists in cache
- Serve cached MP3 if available
- Fall back to original file if cache miss
Implementation in _get_serving_path():
def _get_serving_path(full_path, quality):
"""Return cached file if available, otherwise original"""
if quality and quality != 'original':
cache_path = audio_cache.get_cache_path(full_path, quality)
if os.path.exists(cache_path):
return cache_path
return full_path
π¨ UI IntegrationΒΆ
Cast ButtonΒΆ
The cast button appears automatically when the Cast SDK loads successfully.
HTML:
<button id="cast-button" class="btn btn-outline-secondary" hidden>
<i class="bi bi-cast"></i>
<span class="cast-label">Cast</span>
</button>
JavaScript initialization:
import {
initializeCast,
loadPlaylistAndCast,
extractTracksFromDOM,
stopCasting,
isCasting
} from './chromecast.js';
// Initialize Chromecast
initializeCast();
document.addEventListener('cast:ready', () => {
const castBtn = document.getElementById('cast-button');
if (castBtn) {
castBtn.hidden = false;
castBtn.addEventListener('click', () => {
if (isCasting()) {
stopCasting(); // Stop if currently casting
} else {
// Extract tracks and start casting
const tracks = extractTracksFromDOM();
const currentIndex = window.currentTrackIndex || 0;
loadPlaylistAndCast(tracks, currentIndex);
}
});
}
});
Key Functions:
initializeCast()- Initialize Chromecast (replaces oldinitChromecast())loadPlaylistAndCast(tracks, startIndex)- Start casting (replaces oldcastMixtapePlaylist())extractTracksFromDOM()- Get track list from pageisCasting()- Check if currently castingstopCasting()- End cast session
State classes:
.connected- Applied when casting is active- Default state - Ready to cast
Visual FeedbackΒΆ
When casting starts:
- Cast button shows "connected" state
- Local player UI dims/hides
- "Casting to [Device Name]" indicator appears
- Track controls remain active
During playback:
- Progress bar updates from Chromecast state
- Play/pause button reflects Chromecast state
- Track changes update UI automatically
π± Platform SupportΒΆ
Desktop (Chrome, Edge)ΒΆ
Full support:
- β Cast button
- β Device picker
- β Full queue management
- β Media Session controls
- β Seeking and scrubbing
AndroidΒΆ
Full support:
- β Native Cast integration
- β Notification controls
- β Lock screen controls
- β Background playback
- β Hardware button support
iOSΒΆ
Limited support - requires workarounds:
β οΈ Safari limitations:
- Cast SDK not supported in Safari
- Users must use Chrome for iOS
β Chrome for iOS:
- Full Cast support
- Requires Google Home app installed
- Both devices must be on same WiFi network
Helper message for iOS users:
function showiOSCastHelp() {
const helpHtml = `
<div class="alert alert-info">
<h6>π± Casting from iPhone</h6>
<small>
<strong>To cast to Chromecast:</strong><br>
1. Install Google Home app<br>
2. Use Chrome browser (not Safari)<br>
3. Connect to same WiFi network<br>
<br>
<strong>For best experience:</strong><br>
Add this page to your Home Screen (PWA mode)
</small>
</div>
`;
// Display in modal or banner
}
Detection and help display:
const iOS = detectiOS();
if (iOS && !('chrome' in window)) {
// User is on iOS Safari - show help message
showiOSCastHelp();
}
π§ ConfigurationΒΆ
Cast Application IDΒΆ
Default Media Receiver: CC1AD845
This is Google's standard receiver application for basic media playback.
Set in initializeCastApi():
const applicationID = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID;
const sessionRequest = new chrome.cast.SessionRequest(applicationID);
Queue ConfigurationΒΆ
Settings:
const queueLoadRequest = new chrome.cast.media.QueueLoadRequest(queueItems);
queueLoadRequest.repeatMode = chrome.cast.media.RepeatMode.OFF;
queueLoadRequest.startIndex = window.currentTrackIndex || 0;
queueLoadRequest.autoplay = true;
queueLoadRequest.preloadTime = 5; // Preload 5 seconds of next track
Parameters:
| Parameter | Value | Purpose |
|---|---|---|
repeatMode |
OFF |
No automatic repeat |
startIndex |
Current track | Resume from current position |
autoplay |
true |
Start playing immediately |
preloadTime |
5 |
Buffer next track 5s early |
π§ͺ TestingΒΆ
Testing ChecklistΒΆ
Basic Functionality:
- Cast button appears when Cast SDK loads
- Device picker opens on click
- Chromecast device is discoverable
- Connection establishes successfully
- Mixtape queue loads completely
Playback:
- First track plays automatically
- Audio quality is acceptable
- No audio stuttering or buffering issues
- Queue advances through all tracks
- Last track completes properly
Controls:
- Play/pause works from browser
- Play/pause works from lock screen
- Previous track button works
- Next track button works
- Seeking works (scrub bar)
- Volume control works
State Management:
- Local player is silenced when casting starts
- UI shows "Casting to [Device]" indicator
- Cast button shows "connected" state
- Progress bar updates during playback
- Track metadata displays correctly
Disconnection:
- Stop casting button works
- Local player controls restore
- Media Session clears properly
- UI returns to normal state
- Can restart casting immediately
Edge Cases:
- Network interruption handling
- Chromecast device disconnection
- Browser tab closed while casting
- Multiple browser tabs casting simultaneously
- Quality switching during playback
Test DevicesΒΆ
Recommended test targets:
- Chromecast (Gen 3 or newer)
- Chromecast with Google TV
- Google Nest Audio / Home speakers
- Smart TVs with built-in Chromecast
Debug LoggingΒΆ
Enable verbose Cast logging:
// Add to chromecast.js initialization
window.__CHROMECAST_DEBUG__ = true;
if (window.__CHROMECAST_DEBUG__) {
console.log('π Cast session started:', session);
console.log('π΅ Loading queue:', queueItems);
console.log('π‘ Current media:', currentMedia);
}
Check browser console for:
- Cast SDK load status
- Session establishment logs
- Media state changes
- Error messages
π TroubleshootingΒΆ
Cast Button Not AppearingΒΆ
Symptoms:
- Cast button remains hidden
- No
cast:readyevent fired
Possible causes:
- Cast SDK failed to load
- Not in secure context
- Cast API not available
if (typeof chrome === 'undefined' || !chrome.cast) {
console.error('Chrome Cast API not available');
}
Solution checklist:
- β Verify HTTPS connection
- β Check browser console for errors
- β
Confirm
cast-framework.jsCDN is accessible - β Try different browser (Chrome/Edge recommended)
- β Check network firewall settings
Audio Not Playing on ChromecastΒΆ
Symptoms:
- Cast session connects but no audio plays
- Chromecast shows "buffering" indefinitely
Possible causes:
- CORS headers missing
Solution: Verify CORS headers in Flask:
- Wrong audio URL format
// β Wrong - relative URL
src: 'audio/track.mp3'
// β
Correct - absolute URL
src: 'https://yourdomain.com/play/artist/album/track.mp3'
- MIME type incorrect
- File not accessible
Solution checklist:
- β Verify CORS headers present
- β Confirm audio URLs are absolute
- β Check MIME types are correct
- β Test audio URL directly in browser
- β Review server logs for errors
Seeking Not WorkingΒΆ
Symptoms:
- Seek bar doesn't respond
- Tapping seek position has no effect
- Range requests failing
Possible causes:
- Range requests not supported
- CORS headers incomplete
# Must expose Range header
response.headers["Access-Control-Expose-Headers"] = "Content-Range, Content-Length, Range"
- Wrong HTTP status code
Solution checklist:
- β
Verify
_handle_range_request()is called - β Check 206 responses in network tab
- β
Confirm
Accept-Ranges: bytesheader - β
Verify
Access-Control-Expose-Headersincludes "Range"
Lock Screen Controls Not WorkingΒΆ
Symptoms:
- Lock screen shows controls but they don't work
- Notification controls unresponsive
Possible causes:
- Media Session not updated
- Action handlers not set
// Verify all handlers are set
navigator.mediaSession.setActionHandler('play', () => castPlay());
navigator.mediaSession.setActionHandler('pause', () => castPause());
- Conflicting local Media Session
Solution checklist:
- β
Verify
updateMediaSessionForCast()called on track change - β Check action handlers are set to cast functions
- β Confirm local Media Session is cleared
- β Test with browser DevTools Media panel
iOS Casting IssuesΒΆ
Symptoms:
- Cast button not appearing on iOS
- "Chromecast not found" error
Common causes:
- Using Safari browser
- Google Home app not installed
- Different WiFi networks
Solution checklist:
- β Switch to Chrome for iOS
- β Install Google Home app
- β Verify same WiFi network
- β Show helper message for iOS users
- β Recommend PWA mode for better experience
Queue Not Loading CompletelyΒΆ
Symptoms:
- Only first few tracks load
- Queue stops mid-playlist
Possible causes:
- Track data malformed
- Invalid audio URLs
- Cover art missing
// Check artwork array
artwork: track.cover ? [{
url: `${window.location.origin}/covers/${track.cover}`
}] : []
Solution checklist:
- β Validate track data structure
- β Check audio URL construction
- β Verify cover art URLs
- β Test with smaller playlist first
- β Check browser console for errors
π Additional ResourcesΒΆ
Google Cast DocumentationΒΆ
Related Mixtape Society DocumentationΒΆ
- Playback Routes - Audio streaming implementation
- Android Auto Integration - Alternative casting method
- Audio Caching - Quality management
- Player Controls - UI controls integration
