@@ -40,58 +40,75 @@ function embedUrl(video, opts = {}) {
4040 base . enablejsapi = "1" ;
4141 base . origin = location . origin ;
4242 }
43+ if ( opts . autoplay ) {
44+ base . autoplay = "1" ;
45+ }
4346 const params = new URLSearchParams ( base ) ;
4447 let path ;
4548 if ( parsed . videoId ) {
4649 path = parsed . videoId ;
47- if ( playlistId ) params . set ( "list" , playlistId ) ; // play this video within the (unlisted) playlist
48- } else if ( playlistId ) {
49- // Playlist-only: use the playlist embed endpoint
50+ // Do NOT add list= here — it causes YouTube to handle playlist navigation internally,
51+ // which prevents the ENDED event from firing for our auto-advance logic.
52+ // We manage playlist navigation ourselves via setupAutoAdvance.
53+ } else if ( playlistId && ! parsed . videoId ) {
54+ // Playlist-only (no specific video): use the playlist embed endpoint.
5055 return `https://www.youtube-nocookie.com/embed/videoseries?${ params . toString ( ) } &list=${ encodeURIComponent ( playlistId ) } ` ;
5156 } else {
5257 path = "" ;
5358 }
5459 return `https://www.youtube-nocookie.com/embed/${ path } ?${ params . toString ( ) } ` ;
5560}
5661
57- // Load the YouTube IFrame API once and auto-advance to `nextHash` when the video ends.
58- let ytApiPromise = null ;
59- function loadYouTubeAPI ( ) {
60- if ( window . YT && window . YT . Player ) return Promise . resolve ( ) ;
61- if ( ytApiPromise ) return ytApiPromise ;
62- ytApiPromise = new Promise ( ( resolve ) => {
63- const tag = document . createElement ( "script" ) ;
64- tag . src = "https://www.youtube.com/iframe_api" ;
65- window . onYouTubeIframeAPIReady = ( ) => resolve ( ) ;
66- document . head . appendChild ( tag ) ;
67- } ) ;
68- return ytApiPromise ;
62+ // Auto-advance: load YT IFrame API once, attach player after iframe loads.
63+ let _ytReady = false ;
64+ let _ytReadyCbs = [ ] ;
65+ function onYouTubeIframeAPIReady ( ) {
66+ _ytReady = true ;
67+ _ytReadyCbs . forEach ( fn => fn ( ) ) ;
68+ _ytReadyCbs = [ ] ;
69+ }
70+ function whenYTReady ( fn ) {
71+ if ( _ytReady ) { fn ( ) ; return ; }
72+ _ytReadyCbs . push ( fn ) ;
73+ if ( ! document . querySelector ( 'script[src*="iframe_api"]' ) ) {
74+ const s = document . createElement ( "script" ) ;
75+ s . src = "https://www.youtube.com/iframe_api" ;
76+ document . head . appendChild ( s ) ;
77+ }
6978}
7079
71- function setupAutoAdvance ( nextHash , currentVid ) {
72- const iframe = document . getElementById ( "yt-player" ) ;
73- if ( ! iframe ) return ;
74- loadYouTubeAPI ( ) . then ( ( ) => {
75- // Re-check: user may have navigated away before the API loaded.
76- if ( document . getElementById ( "yt-player" ) !== iframe ) return ;
77- new window . YT . Player ( "yt-player" , {
80+ let _currentPlayer = null ;
81+ let _autoplayNext = false ; // set true when auto-advancing so next video autoplays
82+
83+ function setupAutoAdvance ( nextHash , shouldAutoplay ) {
84+ // Destroy previous player instance cleanly.
85+ if ( _currentPlayer ) {
86+ try { _currentPlayer . destroy ( ) ; } catch ( e ) { }
87+ _currentPlayer = null ;
88+ }
89+
90+ whenYTReady ( ( ) => {
91+ const iframe = document . getElementById ( "yt-player" ) ;
92+ if ( ! iframe ) return ; // user navigated away
93+ _currentPlayer = new YT . Player ( iframe , {
7894 events : {
79- onStateChange : ( e ) => {
80- if ( e . data === window . YT . PlayerState . ENDED && nextHash ) {
81- // Navigate to next video — render() will re-render the playlist with correct highlight
82- location . hash = nextHash ;
83- }
84- if ( e . data === window . YT . PlayerState . PLAYING ) {
85- // Ensure the correct playlist item is highlighted (handles edge cases)
86- document . querySelectorAll ( ".pl-item" ) . forEach ( ( el ) => {
87- const isActive = el . dataset . vid === currentVid ;
88- el . classList . toggle ( "active" , isActive ) ;
89- const nowBadge = el . querySelector ( ".pl-now" ) ;
90- if ( nowBadge ) nowBadge . style . display = isActive ? "" : "none" ;
91- } ) ;
95+ onReady : ( e ) => {
96+ // If this render was triggered by auto-advance, start playing immediately.
97+ if ( shouldAutoplay ) {
98+ try { e . target . playVideo ( ) ; } catch ( err ) { }
9299 }
93100 } ,
94- } ,
101+ onStateChange : ( e ) => {
102+ if ( e . data === YT . PlayerState . ENDED && nextHash ) {
103+ if ( location . hash === nextHash ) {
104+ render ( true ) ; // pass autoplay flag
105+ } else {
106+ _autoplayNext = true ;
107+ location . hash = nextHash ;
108+ }
109+ }
110+ }
111+ }
95112 } ) ;
96113 } ) ;
97114}
@@ -103,13 +120,13 @@ function thumbUrl(video) {
103120 : "https://img.youtube.com/vi/videoseries/hqdefault.jpg" ;
104121}
105122
106- function render ( ) {
123+ function render ( forceAutoplay ) {
107124 const hash = location . hash . slice ( 1 ) ; // e.g. /product/crm/video/crm-1
108125 const parts = hash . split ( "/" ) . filter ( Boolean ) ;
109126 const isHome = parts . length === 0 || parts [ 0 ] !== "product" ;
110127 document . querySelector ( ".navbar" ) . classList . toggle ( "is-home" , isHome ) ;
111128 if ( parts [ 0 ] === "product" && parts [ 2 ] === "video" ) {
112- renderVideo ( parts [ 1 ] , parts [ 3 ] ) ;
129+ renderVideo ( parts [ 1 ] , parts [ 3 ] , forceAutoplay || _autoplayNext ) ;
113130 } else if ( parts [ 0 ] === "product" ) {
114131 renderDashboard ( parts [ 1 ] ) ;
115132 } else {
@@ -125,9 +142,10 @@ function renderHome() {
125142 <div class="icon">${ p . iconSvg ? `<img src="${ p . iconSvg } " alt="${ esc ( p . name ) } " style="width:32px;height:32px;object-fit:contain;" />` : p . icon } </div>
126143 <h3>${ esc ( p . name ) } </h3>
127144 <p>${ esc ( p . tagline ) } </p>
128- <div class="count">${ p . videos . length } video${ p . videos . length === 1 ? "" : "s" } →</div>
145+ ${ p . videos && p . videos . length > 0 ? ` <div class="count">${ p . videos . length } video${ p . videos . length === 1 ? "" : "s" } →</div>` : "" }
129146 </div>`
130147 ) . join ( "" ) ;
148+
131149 app . innerHTML = `
132150 <div class="home-hero-wrap fade">
133151 <div class="hero-orb orb-1"></div>
@@ -197,7 +215,7 @@ function renderDashboard(pid) {
197215 <div class="grid fade">${ cards } </div>` ;
198216}
199217
200- function renderVideo ( pid , vid ) {
218+ function renderVideo ( pid , vid , autoplay ) {
201219 const p = PRODUCTS . find ( ( x ) => x . id === pid ) ;
202220 const idx = p ? p . videos . findIndex ( ( x ) => x . id === vid ) : - 1 ;
203221 const v = idx >= 0 ? p . videos [ idx ] : null ;
@@ -236,7 +254,7 @@ function renderVideo(pid, vid) {
236254 </div>
237255 <div class="detail fade">
238256 <div class="glass player-wrap">
239- <iframe id="yt-player" src="${ embedUrl ( v , { jsapi : true } ) } " title="${ esc ( v . title ) } " allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
257+ <iframe id="yt-player" src="${ embedUrl ( v , { jsapi : true , autoplay : _autoplayNext } ) } " title="${ esc ( v . title ) } " allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
240258 </div>
241259 <div class="glass panel desc-panel">
242260 <h2>${ esc ( v . title ) } </h2>
@@ -257,9 +275,11 @@ function renderVideo(pid, vid) {
257275 </div>
258276 </div>
259277 </div>` ;
260- // Auto-advance to the next video when this one ends.
278+ // Consume the autoplay flag (reset after use so manual navigation doesn't autoplay).
279+ _autoplayNext = false ;
280+ // Auto-advance to the next video when this one ends; pass autoplay flag to start playing.
261281 const next = p . videos [ idx + 1 ] ;
262- setupAutoAdvance ( next ? `#/product/${ p . id } /video/${ next . id } ` : null , v . id ) ;
282+ setupAutoAdvance ( next ? `#/product/${ p . id } /video/${ next . id } ` : null , ! ! autoplay ) ;
263283 // Keep the active playlist item in view.
264284 const active = document . querySelector ( ".pl-item.active" ) ;
265285 if ( active ) active . scrollIntoView ( { block : "nearest" } ) ;
0 commit comments