77 "io"
88 "net"
99 "net/url"
10+ "path"
11+ "path/filepath"
12+ "strings"
1013 "sync"
1114 "time"
1215
@@ -109,13 +112,122 @@ func isTimeoutError(err error) bool {
109112 return false
110113}
111114
115+ func (c * CastClient ) defaultReceiverReady () bool {
116+ app := c .app .App ()
117+ return app != nil && app .AppId == cast .DefaultMediaReceiverAppID && app .TransportId != ""
118+ }
119+
120+ func normalizeMediaTitle (title string , mediaURL string ) string {
121+ if normalized := deriveMediaTitle (title ); normalized != "" {
122+ return normalized
123+ }
124+ return deriveMediaTitle (mediaURL )
125+ }
126+
127+ func deriveMediaTitle (value string ) string {
128+ value = strings .TrimSpace (value )
129+ if value == "" {
130+ return ""
131+ }
132+
133+ if u , err := url .Parse (value ); err == nil && u .Scheme != "" {
134+ if base := path .Base (u .Path ); base != "" && base != "." && base != "/" {
135+ return base
136+ }
137+ if host := strings .TrimSpace (u .Host ); host != "" {
138+ return host
139+ }
140+ }
141+
142+ if base := filepath .Base (value ); base != "" && base != "." && base != string (filepath .Separator ) {
143+ return base
144+ }
145+
146+ return value
147+ }
148+
149+ // Ensure the Default Media Receiver is running before custom media commands.
150+ func (c * CastClient ) ensureDefaultReceiverReady () error {
151+ if c .defaultReceiverReady () {
152+ return nil
153+ }
154+
155+ if err := c .app .Update (); err == nil && c .defaultReceiverReady () {
156+ return nil
157+ }
158+
159+ var lastErr error
160+ for attempt := range 5 {
161+ if ! c .IsConnected () {
162+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Msg ("connection closed during receiver launch, aborting silently" )
163+ return nil
164+ }
165+
166+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Int ("Attempt" , attempt + 1 ).Msg ("launching default receiver" )
167+ if err := LaunchDefaultReceiver (c .conn ); err != nil {
168+ lastErr = err
169+ if isTimeoutError (err ) && attempt < 4 {
170+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Int ("Attempt" , attempt + 1 ).Err (err ).Msg ("timeout, TV may be waking up, retrying..." )
171+ if ! c .IsConnected () {
172+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Msg ("connection closed during retry wait, aborting silently" )
173+ return nil
174+ }
175+ time .Sleep (4 * time .Second )
176+ continue
177+ }
178+ c .Log ().Error ().Str ("Method" , "ensureDefaultReceiverReady" ).Err (err ).Msg ("launch receiver failed" )
179+ return fmt .Errorf ("launch receiver: %w" , err )
180+ }
181+
182+ for i := range 8 {
183+ if ! c .IsConnected () {
184+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Msg ("connection closed during app update, aborting silently" )
185+ return nil
186+ }
187+
188+ if err := c .app .Update (); err != nil {
189+ lastErr = err
190+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Int ("Attempt" , i + 1 ).Err (err ).Msg ("app.Update retry" )
191+ time .Sleep (time .Duration (i + 1 ) * 500 * time .Millisecond )
192+ continue
193+ }
194+
195+ if c .defaultReceiverReady () {
196+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Str ("TransportId" , c .app .App ().TransportId ).Msg ("default receiver ready" )
197+ return nil
198+ }
199+
200+ lastErr = fmt .Errorf ("failed to get default receiver transport ID after retries" )
201+ time .Sleep (time .Duration (i + 1 ) * 500 * time .Millisecond )
202+ }
203+
204+ if attempt < 4 {
205+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Int ("Attempt" , attempt + 1 ).Msg ("receiver not ready, retrying..." )
206+ if ! c .IsConnected () {
207+ c .Log ().Debug ().Str ("Method" , "ensureDefaultReceiverReady" ).Msg ("connection closed during retry wait, aborting silently" )
208+ return nil
209+ }
210+ time .Sleep (4 * time .Second )
211+ continue
212+ }
213+ }
214+
215+ if lastErr == nil {
216+ lastErr = fmt .Errorf ("failed to get default receiver transport ID after retries" )
217+ }
218+
219+ c .Log ().Error ().Str ("Method" , "ensureDefaultReceiverReady" ).Err (lastErr ).Msg ("failed to launch default receiver" )
220+ return lastErr
221+ }
222+
112223// Load loads media from URL onto the Chromecast.
113224// startTime is the position in seconds to start playback from.
114225// duration is the total media duration in seconds (0 to let Chromecast detect).
115226// If subtitleURL is provided, uses custom load command with subtitle tracks.
116227// If live is true, uses StreamType "LIVE" to identify as live stream.
117- func (c * CastClient ) Load (mediaURL string , contentType string , startTime int , duration float64 , subtitleURL string , live bool ) error {
118- c .Log ().Debug ().Str ("Method" , "Load" ).Str ("URL" , mediaURL ).Str ("ContentType" , contentType ).Int ("StartTime" , startTime ).Float64 ("Duration" , duration ).Bool ("HasSubs" , subtitleURL != "" ).Bool ("Live" , live ).Msg ("loading media" )
228+ func (c * CastClient ) Load (mediaURL string , contentType string , title string , startTime int , duration float64 , subtitleURL string , live bool ) error {
229+ mediaTitle := normalizeMediaTitle (title , mediaURL )
230+ c .Log ().Debug ().Str ("Method" , "Load" ).Str ("URL" , mediaURL ).Str ("ContentType" , contentType ).Str ("Title" , mediaTitle ).Int ("StartTime" , startTime ).Float64 ("Duration" , duration ).Bool ("HasSubs" , subtitleURL != "" ).Bool ("Live" , live ).Msg ("loading media" )
119231
120232 // Check if connection is still active, reconnect if needed
121233 // This handles cases where Close() was called but the client is being reused
@@ -126,10 +238,10 @@ func (c *CastClient) Load(mediaURL string, contentType string, startTime int, du
126238 }
127239 }
128240
129- // If no subtitles, no custom duration, and NOT a live stream: use standard app.Load()
241+ // If no subtitles, no custom duration, no title, and NOT a live stream: use standard app.Load()
130242 // For live streams, we MUST use custom LoadWithSubtitles to set StreamType "LIVE"
131243 // (go-chromecast library hardcodes StreamType "BUFFERED" so we need custom path for LIVE)
132- if subtitleURL == "" && duration == 0 && ! live {
244+ if subtitleURL == "" && duration == 0 && mediaTitle == "" && ! live {
133245 // Retry loop for TV wake-up scenarios (timeout errors)
134246 var lastErr error
135247 for attempt := range 5 {
@@ -160,70 +272,42 @@ func (c *CastClient) Load(mediaURL string, contentType string, startTime int, du
160272 // With subtitles or custom duration: launch the app first WITHOUT loading media, then send custom load
161273 // This prevents double playback (first without subs, then with subs queued)
162274 // Retry loop for TV wake-up scenarios
275+ if err := c .ensureDefaultReceiverReady (); err != nil {
276+ return err
277+ }
278+
163279 var lastErr error
164280 for attempt := range 5 {
165281 if ! c .IsConnected () {
166282 c .Log ().Debug ().Str ("Method" , "Load" ).Msg ("connection closed during load, aborting silently" )
167283 return nil
168284 }
169285
170- c .Log ().Debug ().Str ("Method" , "Load" ).Int ("Attempt" , attempt ).Msg ("launching default receiver" )
171- if err := LaunchDefaultReceiver (c .conn ); err != nil {
172- lastErr = err
173- if isTimeoutError (err ) && attempt < 5 {
174- c .Log ().Debug ().Str ("Method" , "Load" ).Int ("Attempt" , attempt ).Err (err ).Msg ("timeout, TV may be waking up, retrying..." )
175- if ! c .IsConnected () {
176- c .Log ().Debug ().Str ("Method" , "Load" ).Msg ("connection closed during retry wait, aborting silently" )
177- return nil
178- }
179- time .Sleep (4 * time .Second )
180- continue
181- }
182- c .Log ().Error ().Str ("Method" , "Load" ).Err (err ).Msg ("launch receiver failed" )
183- return fmt .Errorf ("launch receiver: %w" , err )
184- }
185-
186- // Retry getting app state with backoff (handles "media receiver app not available")
187- var transportId string
188- for i := range 8 {
189- if ! c .IsConnected () {
190- c .Log ().Debug ().Str ("Method" , "Load" ).Msg ("connection closed during app update, aborting silently" )
191- return nil
192- }
193-
194- if err := c .app .Update (); err != nil {
195- c .Log ().Debug ().Str ("Method" , "Load" ).Int ("Attempt" , i + 1 ).Err (err ).Msg ("app.Update retry" )
196- time .Sleep (time .Duration (i + 1 ) * 500 * time .Millisecond )
197- continue
198- }
199- app := c .app .App ()
200- if app != nil && app .TransportId != "" {
201- transportId = app .TransportId
202- c .Log ().Debug ().Str ("Method" , "Load" ).Str ("TransportId" , transportId ).Msg ("got transport ID" )
203- break
204- }
205- time .Sleep (time .Duration (i + 1 ) * 500 * time .Millisecond )
286+ app := c .app .App ()
287+ transportId := ""
288+ if app != nil {
289+ transportId = app .TransportId
206290 }
207291
208292 if transportId == "" {
209- lastErr = fmt .Errorf ("failed to get transport ID after retries " )
210- if attempt < 5 {
211- c .Log ().Debug ().Str ("Method" , "Load" ).Int ("Attempt" , attempt ).Msg ("no transport ID, TV may be waking up , retrying..." )
293+ lastErr = fmt .Errorf ("failed to get transport ID after receiver launch " )
294+ if attempt < 4 {
295+ c .Log ().Debug ().Str ("Method" , "Load" ).Int ("Attempt" , attempt + 1 ).Msg ("no transport ID, retrying..." )
212296 if ! c .IsConnected () {
213297 c .Log ().Debug ().Str ("Method" , "Load" ).Msg ("connection closed during retry wait, aborting silently" )
214298 return nil
215299 }
216300 time .Sleep (4 * time .Second )
217301 continue
218302 }
219- c .Log ().Error ().Str ("Method" , "Load" ).Msg ("failed to get transport ID" )
303+ c .Log ().Error ().Str ("Method" , "Load" ).Err ( lastErr ). Msg ("failed to get transport ID" )
220304 return lastErr
221305 }
222306
223307 // For live streams: load PAUSED then immediately send PLAY command
224308 // This simulates a "fast click" which avoids the 20-30s buffer that autoplay=true triggers
225309 autoplay := ! live // Only autoplay if NOT a live stream
226- err := LoadWithSubtitles (c .conn , transportId , mediaURL , contentType , startTime , duration , subtitleURL , live , autoplay )
310+ err := LoadWithSubtitles (c .conn , transportId , mediaURL , contentType , startTime , duration , subtitleURL , mediaTitle , live , autoplay )
227311 if err != nil {
228312 lastErr = err
229313 if isTimeoutError (err ) && attempt < 5 {
@@ -278,8 +362,9 @@ func (c *CastClient) Load(mediaURL string, contentType string, startTime int, du
278362// Unlike Load, this skips launching the receiver.
279363// Use this when the receiver is already playing media and you want to load new content.
280364// If live is true, uses StreamType "LIVE" to identify as live stream.
281- func (c * CastClient ) LoadOnExisting (mediaURL string , contentType string , startTime int , duration float64 , subtitleURL string , live bool ) error {
282- c .Log ().Debug ().Str ("Method" , "LoadOnExisting" ).Str ("URL" , mediaURL ).Str ("ContentType" , contentType ).Int ("StartTime" , startTime ).Float64 ("Duration" , duration ).Bool ("HasSubs" , subtitleURL != "" ).Bool ("Live" , live ).Msg ("loading media on existing receiver" )
365+ func (c * CastClient ) LoadOnExisting (mediaURL string , contentType string , title string , startTime int , duration float64 , subtitleURL string , live bool ) error {
366+ mediaTitle := normalizeMediaTitle (title , mediaURL )
367+ c .Log ().Debug ().Str ("Method" , "LoadOnExisting" ).Str ("URL" , mediaURL ).Str ("ContentType" , contentType ).Str ("Title" , mediaTitle ).Int ("StartTime" , startTime ).Float64 ("Duration" , duration ).Bool ("HasSubs" , subtitleURL != "" ).Bool ("Live" , live ).Msg ("loading media on existing receiver" )
283368
284369 // LoadOnExisting requires an active connection (it's designed for already-running receivers)
285370 // Unlike Load(), we don't auto-reconnect because that would defeat the optimization purpose
@@ -310,7 +395,7 @@ func (c *CastClient) LoadOnExisting(mediaURL string, contentType string, startTi
310395 }
311396
312397 // For LoadOnExisting, always autoplay since it's for seek operations on active content
313- err := LoadWithSubtitles (c .conn , transportId , mediaURL , contentType , startTime , duration , subtitleURL , live , true )
398+ err := LoadWithSubtitles (c .conn , transportId , mediaURL , contentType , startTime , duration , subtitleURL , mediaTitle , live , true )
314399 if err != nil {
315400 c .Log ().Error ().Str ("Method" , "LoadOnExisting" ).Err (err ).Msg ("failed" )
316401 } else {
0 commit comments