2626 */
2727
2828import angular from "angular" ;
29+ import { WebVTT } from "vtt.js" ;
2930
3031import { getCookie } from "../getCookie" ;
3132
@@ -38,7 +39,7 @@ import { getCookie } from "../getCookie";
3839 * @description
3940 * CDS record controller.
4041 */
41- function cdsRecordController ( $scope , $sce , $http ) {
42+ function cdsRecordController ( $scope , $sce , $http , $timeout ) {
4243 // Parameters
4344
4445 // Assign the controller to `vm`
@@ -53,11 +54,216 @@ function cdsRecordController($scope, $sce, $http) {
5354 // Record Warn - if the cdsRecord has any warning
5455 vm . cdsRecordWarning = null ;
5556
57+ $scope . transcriptsByLanguage = { } ;
58+ $scope . transcript = [ ] ;
59+ $scope . filteredTranscript = [ ] ;
60+ $scope . selectedTranscriptLanguage = null ;
61+
5662 const REQUEST_HEADERS = {
5763 "Content-Type" : "application/json" ,
5864 "X-CSRFToken" : getCookie ( "csrftoken" ) ,
5965 } ;
6066
67+ $scope . seekTo = function ( timecode ) {
68+ const player = window . top . player ;
69+ if ( player ) {
70+ player . currentTime = timecode ;
71+ if ( player . paused ) {
72+ player . play ( ) . catch ( function ( err ) {
73+ console . warn ( "Autoplay might be blocked by the browser:" , err ) ;
74+ } ) ;
75+ }
76+ } else {
77+ console . warn ( "Player not available" ) ;
78+ }
79+ } ;
80+
81+ $scope . toggleTranscript = function ( ) {
82+ $scope . showTranscript = ! $scope . showTranscript ;
83+
84+ // Jump to Transcriptions section
85+ if ( $scope . showTranscript ) {
86+ setTimeout ( function ( ) {
87+ const el = document . getElementById ( "transcriptionsSection" ) ;
88+ if ( el ) {
89+ const rect = el . getBoundingClientRect ( ) ;
90+ const isVisible =
91+ rect . top >= 0 &&
92+ rect . bottom <=
93+ ( window . innerHeight || document . documentElement . clientHeight ) ;
94+
95+ if ( ! isVisible ) {
96+ const topOffset = rect . top + window . scrollY - 60 ;
97+ window . scrollTo ( { top : topOffset , behavior : "smooth" } ) ;
98+ }
99+ }
100+ } , 100 ) ;
101+ }
102+ } ;
103+
104+ $scope . parseVttFromUrl = function ( url , type , lang ) {
105+ fetch ( url )
106+ . then ( ( res ) => res . text ( ) )
107+ . then ( function ( vttText ) {
108+ const parser = new WebVTT . Parser ( window , WebVTT . StringDecoder ( ) ) ;
109+ const cues = { } ;
110+
111+ parser . oncue = function ( cue ) {
112+ cues [ cue . text ] = {
113+ start : cue . startTime ,
114+ end : cue . endTime ,
115+ text : cue . text ,
116+ } ;
117+ } ;
118+
119+ parser . parse ( vttText ) ;
120+ parser . flush ( ) ;
121+
122+ $timeout ( function ( ) {
123+ if ( type === "transcript" ) {
124+ $scope . transcriptsByLanguage [ lang ] = cues ;
125+
126+ // Use the first one that loads
127+ if ( ! $scope . selectedTranscriptLanguage ) {
128+ $scope . transcript = cues ;
129+ $scope . filterTranscript ( ) ;
130+ $scope . selectedTranscriptLanguage = lang ;
131+ }
132+ } else {
133+ console . warn ( "Unknown type for VTT parsing:" , type ) ;
134+ }
135+ } ) ;
136+ } )
137+ . catch ( function ( err ) {
138+ console . error ( "VTT parsing failed" , err ) ;
139+ } ) ;
140+ } ;
141+
142+ $scope . transcriptSearch = "" ;
143+
144+ $scope . filterTranscript = function ( ) {
145+ var searchTerm = $scope . transcriptSearch . toLowerCase ( ) ;
146+ $scope . filteredTranscript = Object . values ( $scope . transcript ) . filter (
147+ function ( line ) {
148+ return (
149+ ! searchTerm ||
150+ ( line . text && line . text . toLowerCase ( ) . indexOf ( searchTerm ) !== - 1 )
151+ ) ;
152+ }
153+ ) ;
154+ } ;
155+
156+ $scope . $watch ( "transcript" , function ( newVal ) {
157+ if ( newVal ) $scope . filterTranscript ( ) ;
158+ } ) ;
159+
160+ $scope . $watch ( "record" , function ( newVal ) {
161+ if ( newVal ) {
162+ $scope . initVttLoad ( newVal ) ;
163+ }
164+ } ) ;
165+
166+ $scope . initVttLoad = function ( record ) {
167+ console . log ( "Initializing VTT load for record:" , record ) ;
168+ const files = record . metadata . _files || [ ] ;
169+
170+ // Subtitles (transcripts)
171+ const transcriptVttFiles = files . filter (
172+ ( f ) => f . context_type === "subtitle" && f . content_type === "vtt"
173+ ) ;
174+
175+ // Step 2: If found, load it
176+ transcriptVttFiles . forEach ( ( file ) => {
177+ const lang = file . tags . language || "unknown" ;
178+ if ( file . links ?. self ) {
179+ $scope . parseVttFromUrl ( file . links . self , "transcript" , lang ) ;
180+ } else {
181+ console . warn ( "No subtitle file found." ) ;
182+ }
183+ } ) ;
184+ } ;
185+
186+ $scope . setTranscriptLanguage = function ( lang ) {
187+ if ( $scope . transcriptsByLanguage [ lang ] ) {
188+ $scope . transcript = $scope . transcriptsByLanguage [ lang ] ;
189+ $scope . filterTranscript ( ) ;
190+ } else {
191+ console . warn ( "Transcript not found for language:" , lang ) ;
192+ }
193+ } ;
194+
195+ // Follow transcriptions
196+ $scope . currentTranscriptLine = null ;
197+ function getScrollableParent ( el ) {
198+ while ( el && el !== document . body ) {
199+ const style = window . getComputedStyle ( el ) ;
200+ const overflowY = style . overflowY ;
201+ if ( overflowY === "auto" || overflowY === "scroll" ) {
202+ return el ;
203+ }
204+ el = el . parentElement ;
205+ }
206+ return null ;
207+ }
208+ function updateTranscriptHighlight ( ) {
209+ const player = window . top . player ;
210+ if ( ! player || ! $scope . transcript ) return ;
211+
212+ const currentTime = player . currentTime ;
213+ const lines = Object . values ( $scope . transcript ) ;
214+
215+ for ( let i = 0 ; i < lines . length ; i ++ ) {
216+ const line = lines [ i ] ;
217+ if ( currentTime >= line . start && currentTime <= line . end ) {
218+ if ( $scope . currentTranscriptLine !== line ) {
219+ $scope . currentTranscriptLine = line ;
220+ $scope . $applyAsync ( ) ; // Trigger Angular update
221+
222+ // Auto-scroll to the active line
223+ setTimeout ( ( ) => {
224+ const el = document . querySelector ( ".transcript-line.active" ) ;
225+ const container = getScrollableParent ( el ) ;
226+
227+ if ( el && container ) {
228+ const elRect = el . getBoundingClientRect ( ) ;
229+ const containerRect = container . getBoundingClientRect ( ) ;
230+
231+ const currentScroll = container . scrollTop ;
232+ const topOffset = elRect . top - containerRect . top ;
233+
234+ const targetScroll = currentScroll + topOffset - 10 ;
235+
236+ container . scrollTo ( {
237+ top : targetScroll ,
238+ behavior : "smooth" ,
239+ } ) ;
240+ }
241+ } , 50 ) ;
242+ }
243+ return ;
244+ }
245+ }
246+
247+ $scope . currentTranscriptLine = null ;
248+ $scope . $applyAsync ( ) ;
249+ }
250+
251+ let transcriptTimer = setInterval ( updateTranscriptHighlight , 100 ) ;
252+
253+ $scope . $on ( "$destroy" , function ( ) {
254+ clearInterval ( transcriptTimer ) ;
255+ } ) ;
256+
257+ $scope . convertToMinutesSeconds = function ( seconds ) {
258+ const minutes = Math . floor ( seconds / 60 ) ;
259+ const secs = Math . floor ( seconds % 60 ) ;
260+
261+ // Pad with zero if needed
262+ const paddedSecs = secs < 10 ? "0" + secs : secs ;
263+
264+ return `${ minutes } :${ paddedSecs } ` ;
265+ } ;
266+
61267 /**
62268 * Trust iframe url
63269 * @memberof cdsRecordController
@@ -180,7 +386,7 @@ function cdsRecordController($scope, $sce, $http) {
180386 $scope . $on ( "cds.record.loading.stop" , cdsRecordLoadingStop ) ;
181387}
182388
183- cdsRecordController . $inject = [ "$scope" , "$sce" , "$http" ] ;
389+ cdsRecordController . $inject = [ "$scope" , "$sce" , "$http" , "$timeout" ] ;
184390
185391////////////
186392
0 commit comments