class Player_Class { constructor(Dings_Json, Html_Id, Container, On_Play_Callback = false, On_Pause_Callback = false, On_Loaded_Callback = false) { this.Html_Id = Html_Id; this.Dings_Json = Dings_Json; this.Container = Container this.Subtitles_Enabled = false this.On_Play_Callback = On_Play_Callback this.On_Pause_Callback = On_Pause_Callback this.On_Loaded_Callback = On_Loaded_Callback console.log("Player_Class: " + Html_Id) } Add_Html_Class(Html_Class) { var Player_Html = document.getElementById(this.Html_Id) Player_Html.classList.add(Html_Class) } Remove_Html_Class(Html_Class) { var Player_Html = document.getElementById(this.Html_Id) Player_Html.classList.remove(Html_Class) } Get_Current_Width() { let Aspect_Ratio = this.Dings_Json.Aspect_Ratio.Width / this.Dings_Json.Aspect_Ratio.Height let Width_Real console.log("CWidth:" + this.Container.clientWidth) console.log("CHeight:" + this.Container.clientHeight) if (this.Container.clientWidth / this.Container.clientHeight > Aspect_Ratio) { Width_Real = Math.round(this.Container.clientWidth * Aspect_Ratio) } else { Width_Real = this.Container.clientWidth } console.log("Width:" + Width_Real) return Width_Real } Get_Current_Height() { let Aspect_Ratio = this.Dings_Json.Aspect_Ratio.Width / this.Dings_Json.Aspect_Ratio.Height let Height_Real = this.Get_Current_Width() / Aspect_Ratio console.log("Height:" + Height_Real) return Height_Real } } class Player_Local_Class extends Player_Class { constructor(Dings_Json, Html_Id, Container, On_Play_Callback, On_Pause_Callback, On_Loaded_Callback, On_Time_Update_Callback) { super(Dings_Json, Html_Id, Container, On_Play_Callback, On_Pause_Callback, On_Loaded_Callback); this.Player = document.getElementById(this.Html_Id); this.Subtitles_Setup() this.Set_Current_Time(0) this.Player.onpause = On_Pause_Callback this.Player.onplay = On_Play_Callback this.Player.addEventListener('loadeddata', this.On_Loaded_Callback) this.Player.addEventListener('timeupdate', On_Time_Update_Callback) } Update() { // this.Player.setSize(this.Get_Current_Width(), this.Get_Current_Height()); } Is_Playing() { return !!(this.Player.currentTime > 0 && !this.Player.paused && !this.Player.ended && this.Player.readyState > 2) } Get_Current_Time() { return this.Player.currentTime } Set_Current_Time(Seconds) { this.Player.currentTime = Seconds; } Get_Playback_Rate(Rate) { return this.Player.playbackRate } Set_Playback_Rate(Rate) { this.Player.playbackRate = Rate } Pause() { this.Player.pause(); } Play() { this.Player.play(); } Set_Volume(Level) { this.Player.volume = Level / 100 if (Level == 0) this.Player.muted = true else this.Player.muted = false } Subtitles_Setup() { if (this.Subtitles_Enabled) this.Subtitles_Set_Language(this.Subtitles_Language_Code) else this.Subtitles_Disable() } Subtitles_Disable() { for (var i = 0; i < this.Player.textTracks.length; i++) { console.log("Track: " + i) this.Player.textTracks[i].mode = 'disabled'; } this.Subtitles_Enabled = false } Get_Index_For_Language_Code(Language_Code) { for (let Index = 0; Index < this.Dings_Json.Subtitle_File_List.length; Index++) { console.log(Index + " -> " + this.Dings_Json.Subtitle_File_List[Index]) if (Language_Code == this.Dings_Json.Subtitle_File_List[Index][1]) return Index } return -1 } Subtitles_Set_Language(Language_Code) { this.Subtitles_Language_Code = Language_Code this.Subtitles_Enabled = true for (var i = 0; i < this.Player.textTracks.length; i++) { console.log("Track: " + i) this.Player.textTracks[i].mode = 'disabled'; } const Index = this.Get_Index_For_Language_Code(Language_Code) console.log("XXX Subtitles: " + Index) this.Player.textTracks[Index].mode = 'showing'; } } // YouTube-Player Api-Doc: https://developers.google.com/youtube/iframe_api_reference class Player_YouTube_Class extends Player_Class { static Player_List = [] static Api_Ready() { for (const Player of Player_YouTube_Class.Player_List) { Player.YouTube_on_Iframe_Api_Ready() } } constructor(Dings_Json, Html_Id, Container, On_Play_Callback, On_Pause_Callback, On_Loaded_Callback) { console.log("Youtube constructor") super(Dings_Json, Html_Id, Container, On_Play_Callback, On_Pause_Callback, On_Loaded_Callback); this.YouTube_State = null; this.Initialized = false this.Player = false this.Volume_Level = 100 this.Current_Time_Seconds = 0 this.Playing = false this.YouTube_Load_Api(); Player_YouTube_Class.Player_List.push(this) } YouTube_Load_Api() { const Tag = document.createElement("script"); Tag.src = "https://www.youtube.com/iframe_api"; const First_Script_Tag = document.getElementsByTagName("script")[0]; First_Script_Tag.parentNode.insertBefore(Tag, First_Script_Tag); // Callback function to be called when YouTube iframe API is loaded window.onYouTubeIframeAPIReady = Player_YouTube_Class.Api_Ready; } YouTube_on_Iframe_Api_Ready() { console.log("YouTube iframe API is ready: " + this.Html_Id); console.log(" width: " + this.Get_Current_Width()) console.log(" tag : " + this.Dings_Json.YouTube_Id) // Initialize the YouTube Player this.Player = new YT.Player(this.Html_Id, { width: this.Get_Current_Width(), height: this.Get_Current_Height(), videoid: this.Dings_Json.YouTube_Id, playerVars: { color: "white", // playlist: this.Dings_Json.YouTube_Id + "," + "FG0fTKAqZ5g", playlist: this.Dings_Json.YouTube_Id, }, events: { onReady: this.YouTube_On_Ready.bind(this), onStateChange: this.YouTube_On_State_Change.bind(this), }, }); console.log(" - Player: " + this.Player) } Get_State_String(Player_State) { let State_String = "" switch(Player_State) { case YT.PlayerState.ENDED: State_String = "ENDED" break case YT.PlayerState.PLAYING: State_String = "PLAYING" break case YT.PlayerState.PAUSED: State_String = "PAUSED" break case YT.PlayerState.BUFFERING: State_String = "BUFFERING" break case YT.PlayerState.CUED: State_String = "CUED" break case -1: State_String = "NOT-STARTED" break default: State_String = "UNKNOWN (" + Player_State + ")" break } return State_String } YouTube_On_State_Change(Event) { this.YouTube_State = Event.data; console.log("State change: "); console.log(this.Get_State_String(this.YouTube_State)) if (!this.Initialized && this.YouTube_State == YT.PlayerState.PLAYING) { console.log(" - Init") this.Set_Current_Time(this.Current_Time_Seconds) this.Subtitles_Setup() this.Set_Volume(this.Volume_Level) this.Initialized = true } if (this.YouTube_State == YT.PlayerState.PAUSED) this.On_Pause_Callback(Event) if (this.YouTube_State == YT.PlayerState.PLAYING) this.On_Play_Callback(Event) // var Tracks = this.Player_YouTube.getOption('captions', 'tracklist'); // this.Enable_YouTube_Subtitles() // this.Set_YouTube_Caption_Language("en") } YouTube_On_Ready() { console.log("Player is ready"); this.Set_Current_Time(this.Current_Time_Seconds) this.Subtitles_Setup() if (this.Playing) this.Player.playVideo() else this.Player.stopVideo() this.On_Loaded_Callback(Event) } Update() { let Width_Real, Height_Real console.log("Updatae") console.log(this.Container.clientWidth) console.log(this.Player) let Width = this.Get_Current_Width() let Height = this.Get_Current_Height() this.Player.setSize(Width, Height) } Is_Playing() { if (typeof YT !== 'undefined') return this.YouTube_State == YT.PlayerState.PLAYING else return false } Pause() { this.Playing = false if (this.Player) this.Player.pauseVideo(); } Play() { this.Playing = true if (this.Player) this.Player.playVideo(); } Get_Current_Time() { if (this.Player) return this.Player.getCurrentTime() else return this.Current_Time_Seconds } Set_Current_Time(Seconds) { this.Current_Time_Seconds = Seconds if (this.Player) this.Player.seekTo(Seconds); } Get_Playback_Rate() { return this.Player.getPlaybackRate() } Set_Playback_Rate(Rate) { this.Player.setPlaybackRate(Rate) } Set_Volume(Level) { this.Volume_Level = Level if (!this.Player) return if (Level == 0) { this.Player.mute() } else { this.Player.unMute() this.Player.setVolume(Level) } } Subtitles_Setup() { if (this.Subtitles_Enabled) this.Subtitles_Set_Language(this.Subtitles_Language_Code) else this.Subtitles_Disable() } Subtitles_Enable() { this.Subtitles_Enabled = true if (!this.Player) return this.Player.loadModule("captions"); //Works for html5 ignored by AS3 this.Player.loadModule("cc"); //Works for AS3 ignored by html5 } Subtitles_Disable() { this.Subtitles_Enabled = false if (!this.Player) return this.Player.unloadModule("captions"); //Works for html5 ignored by AS3 this.Player.unloadModule("cc"); //Works for AS3 ignored by html5 } Subtitles_Set_Language(Language_Code) { this.Subtitles_Language_Code = Language_Code this.Subtitles_Enabled = true if (!this.Player) return this.Subtitles_Enable() this.Player.setOption("captions", "track", {"languageCode": Language_Code}); //Works for html5 ignored by AS3 this.Player.setOption("cc", "track", {"languageCode": Language_Code}); //Works for AS3 ignored by html5 } } class Player_Audio_Class extends Player_Class { constructor(Dings_Json, Html_Id, Container) { super(Dings_Json, Html_Id, Container); this.Dings_Json = Dings_Json; this.Player = document.getElementById(this.Html_Id) } Update() { } Pause() { this.Player.pause(); } Play() { this.Player.play(); } Get_Playback_Rate(Rate) { return this.Player.playbackRate } Set_Playback_Rate(Rate) { this.Player.playbackRate = Rate } Get_Current_Time() { return this.Player.currentTime } Set_Current_Time(Seconds) { this.Player.currentTime = Seconds; } } class Dings_Video_Player_Class extends Dings_App_Class { constructor(Dings_Json, Html_Id) { super(Dings_Json, Html_Id) console.log("Url: " + window.location.href) this.Container = document.getElementById(this.Html_Id + ".Container") // console.log(this.Html_Id + ".Full_Screen_Container") const Aspect_Ratio_From_Json = Dings_Json["Aspect_Ratio"] this.Container.style.aspectRatio = Aspect_Ratio_From_Json.Width / Aspect_Ratio_From_Json.Height this.Player_Local = new Player_Local_Class( Dings_Json, this.Html_Id + ".Local_Player", this.Container, this.On_Play_Callback.bind(this), this.On_Pause_Callback.bind(this), this.On_Loaded_Callback.bind(this), this.On_Time_Update_Callback.bind(this) ) this.Player_YouTube = new Player_YouTube_Class( Dings_Json, this.Html_Id + ".YouTube_Player", this.Container, this.On_Play_Callback.bind(this), this.On_Pause_Callback.bind(this), this.On_Loaded_Callback.bind(this), this.On_Time_Update_Callback.bind(this) ) this.Player_Video_Current = this.Player_YouTube this.Player_Audio_Dict = {} for (const Audio_Track of Dings_Json.Audio_File_List) { const Audio_File_Name = Audio_Track[0] const Language_Tag = Audio_Track[1] const Audio_Player = new Player_Audio_Class(Dings_Json, this.Html_Id + ".Audio." + Language_Tag, this.Container) this.Player_Audio_Dict[Language_Tag] = Audio_Player Audio_Player.Pause() } this.Player_Audio_Current = false this.Sync_Timer = false this.Sync_Interval_Ms = 1000 this.Sync_Last_Diff_S = 0 this.Select_Toc = document.getElementById(this.Html_Id + ".Toc-Select"); this.Select_Toc.addEventListener('change', this.Setup_Toc.bind(this)) this.Select_Subtitles = document.getElementById(this.Html_Id + ".Subtitle-Select"); this.Select_Subtitles.addEventListener('change', this.Setup_Subtitles.bind(this)) this.Select_Player = document.getElementById(this.Html_Id + ".Player-Select"); this.Select_Player.addEventListener('change', this.Setup_Player.bind(this)) this.Select_Audio_Language = document.getElementById(this.Html_Id + '.Audio-Language-Select') this.Select_Audio_Language.addEventListener('change', this.Setup_Audio.bind(this)) if (this.Select_Audio_Language.options.length == 1) this.Select_Audio_Language.disabled = true let Dict = Dings_Lib.Dict_From_Url(Html_Id) if ("Time" in Dict) this.Set_Current_Time(Dict["Time"]) if ("Subtitles" in Dict) this.Select_Subtitles.value = Dict["Subtitles"] if ("Toc" in Dict) this.Select_Toc.value = Dict["Toc"] if ("Audio" in Dict) this.Select_Audio_Language.value = Dict["Audio"] if ("Player" in Dict) this.Select_Player.value = Dict["Player"] if (Local_Storage.Current_User == "Unknown") { console.log("User: " + Local_Storage.Current_User + " -> Disable Local-Player") this.Select_Player.value = "youtube" this.Select_Player.disabled = true this.Select_Player.title="Other Players only for registered Users" /* this.Select_Audio_Language.disabled = true this.Select_Audio_Language.title="Other Languages only for registered Users" */ } this.Setup_Toc() this.Player_Initialized = 0 // Local Player: Keep External Audio Alive After Seeking // Without this, some browsers pause/mute the audio element after a seek. this.Player_Local.Player.addEventListener('seeking', this.On_Local_Video_Seeking_Callback.bind(this)) this.Player_Local.Player.addEventListener('seeked', this.On_Local_Video_Seeked_Callback.bind(this)) this.Setup_Player() } Get_State_Dict() { let State_Dict = { "Player": this.Select_Player.value, "Audio": this.Select_Audio_Language.value, "Subtitles": this.Select_Subtitles.value, "Toc": this.Select_Toc.value, "Time": this.Get_Current_Time() } return State_Dict } On_Time_Update_Callback(Event) { } On_Local_Video_Seeking_Callback(Event) { // Only relevant for Local Player + external Audio Track if (this.Player_Video_Current !== this.Player_Local) return if (!this.Player_Audio_Current) return // Stop audio immediately to avoid "stuck" states while seeking try { this.Player_Audio_Current.Pause() } catch (E) { } } On_Local_Video_Seeked_Callback(Event) { // Only relevant for Local Player + external Audio Track if (this.Player_Video_Current !== this.Player_Local) return if (!this.Player_Audio_Current) return const T = this.Player_Local.Get_Current_Time() try { this.Player_Audio_Current.Set_Current_Time(T) } catch (E) { } // If video is currently playing, restart audio right away. // This usually runs in the user-gesture context of the seek, // so browsers allow the play() call. if (this.Player_Local.Is_Playing()) { try { const P = this.Player_Audio_Current.Player.play() if (P && typeof P.catch === "function") { P.catch(() => {}) } } catch (E) { } } // Force an immediate sync pass this.Sync_Audio() } On_Loaded_Callback(Event) { this.Player_Initialized += 1 console.log("Player Loaded: " + this.Player_Initialized) if (this.Player_Initialized == 2) { this.Setup_Subtitles() this.Setup_Audio() } } On_Play_Callback(Event) { console.log("Play_Callback") if (Event.target != this.Player_Video_Current.Player) { console.log(" from Other") return } console.log(" from Player") this.Sync_Audio_Start() } On_Pause_Callback(Event) { console.log("Pause_Callback") if (Event.target != this.Player_Video_Current.Player) { console.log(" from Other") return } console.log(" from Player") this.Sync_Audio_Stop() } Setup_Audio() { var Language_Tag_Selected = this.Select_Audio_Language.value; console.log("Audio: Switch: " + Language_Tag_Selected) // Setup Audio if (this.Player_Audio_Current) { this.Sync_Audio_Stop() this.Player_Audio_Current.Pause() } if (Language_Tag_Selected == "original") { console.log("Original") this.Player_Video_Current.Set_Volume(100) /* XXX */ this.Player_Audio_Current = false } else { console.log("Audio: " + this.Player_Audio_Dict[Language_Tag_Selected]) this.Player_YouTube.Set_Volume(0) this.Player_Local.Set_Volume(0) this.Player_Audio_Current = this.Player_Audio_Dict[Language_Tag_Selected] this.Sync_Audio_Start() } console.log("Player: " + this.Player_Audio_Current) console.log(" Audio: " + Language_Tag_Selected) } Sync_Audio_Start() { console.log("Audio Sync Start") console.log(" Player: " + this.Player_Audio_Current) if (!this.Player_Audio_Current) return if (!this.Player_Video_Current.Is_Playing()) return this.Sync_Audio_Stop() console.log(" Start") this.Player_Audio_Current.Play(); this.Sync_Audio() this.Sync_Timer = setInterval(this.Sync_Audio.bind(this), this.Sync_Interval_Ms) } Sync_Audio_Stop() { console.log("Audio Sync Stop") if (!this.Player_Audio_Current) return this.Player_Audio_Current.Pause(); this.Sync_Timer = clearInterval(this.Sync_Timer) } Sync_Audio_Guess(Time_Diff_S, Time_Diff_S_Abs, Current_Time_Video, Current_Time_Audio) { console.log("Guess-Mode: " + Time_Diff_S_Abs) if (Time_Diff_S_Abs > 0.5) { // For Start-Up console.log(" - Hard") this.Player_Audio_Current.Set_Current_Time(Current_Time_Video) this.Sync_Last_Diff_S = 0 return } if (Time_Diff_S_Abs > 0.1) { // When Audio and Video is running console.log(" - Adjust: " + Time_Diff_S) this.Player_Audio_Current.Set_Current_Time(Current_Time_Video - this.Sync_Last_Diff_S) if (this.Sync_Last_Diff_S > 0) this.Sync_Last_Diff_S = 0 else this.Sync_Last_Diff_S = Time_Diff_S } } Sync_Audio_Speed(Time_Diff_S, Time_Diff_S_Abs, Current_Time_Video, Current_Time_Audio) { console.log("Speed-Mode") if (Time_Diff_S_Abs > 0.2) { console.log(" - Hard") this.Player_Audio_Current.Set_Current_Time(Current_Time_Video) this.Player_Audio_Current.Set_Playback_Rate(1) } else if (Time_Diff_S_Abs > 0.05) { const Min_Speed = 0.1 const Max_Speed = 10.0 let Speed = 1 - Time_Diff_S / (this.Sync_Interval_Ms / 1000) Speed = Math.max(Min_Speed, Math.min(Max_Speed, Speed)); console.log(" - Adjust: " + this.Player_Audio_Current.Player.playbackRate + " -> " + Speed) this.Player_Audio_Current.Set_Playback_Rate(Speed) } } Sync_Audio() { let Current_Time_Video = this.Player_Video_Current.Get_Current_Time() let Current_Time_Audio = this.Player_Audio_Current.Get_Current_Time() var Time_Diff_S = Current_Time_Audio - Current_Time_Video let Time_Diff_S_Abs = Math.abs(Time_Diff_S) console.log("Sync: " + Current_Time_Audio + " <-> " + Current_Time_Video + " " + this.Player_Audio_Current.Player.playbackRate) console.log(" - Diff: " + Time_Diff_S) if (true) this.Sync_Audio_Guess(Time_Diff_S, Time_Diff_S_Abs, Current_Time_Video, Current_Time_Audio) else this.Sync_Audio_Speed(Time_Diff_S, Time_Diff_S_Abs, Current_Time_Video, Current_Time_Audio) } Disable_Subtitles() { this.Player_YouTube.Subtitles_Disable() this.Player_Local.Subtitles_Disable() } Setup_Toc() { const Selected_Language_Tag = this.Select_Toc.value for (let Media_Toc of this.Dings_Json.Media_Toc_File_List) { let Language_Tag = Media_Toc[1] // Hide all table of contents let Media_Toc_Html = document.getElementById(this.Html_Id + ".Toc." + Language_Tag) console.log("XXX: " + this.Html_Id + ".Toc." + Language_Tag) console.log(Media_Toc_Html) Media_Toc_Html.style.display = "None" } // Show the selected table of contents if (Selected_Language_Tag == "None") { document.getElementById(this.Html_Id + ".Toc").style.display = "None" } else { document.getElementById(this.Html_Id + ".Toc").style.display = "" document.getElementById(this.Html_Id + ".Toc." + Selected_Language_Tag).style.display = "Block" } } Enable_Subtitles() { this.Player_YouTube.Subtitles_Set_Language(this.Select_Subtitles.value) this.Player_Local.Subtitles_Set_Language(this.Select_Subtitles.value) } Setup_Subtitles() { let Subtitle_String = this.Select_Subtitles.value console.log("Setupt Subtitles: " + Subtitle_String) if (Subtitle_String == "off") this.Disable_Subtitles() else this.Enable_Subtitles() } Update() { this.Player_Local.Update() this.Player_YouTube.Update() } Setup_Player() { let Player_String = this.Select_Player.value let Player_New let Player_Old = this.Player_Video_Current if (Player_String == "youtube") Player_New = this.Player_YouTube else Player_New = this.Player_Local Player_New.Set_Current_Time(Player_Old.Get_Current_Time()) this.Player_Video_Current = Player_New if (Player_Old.Is_Playing()) { Player_New.Play() Player_Old.Pause() } Player_Old.Remove_Html_Class("Dings_Video_Toggle-Child"); Player_Old.Add_Html_Class("Dings_Video_Toggle-Parent") Player_New.Remove_Html_Class("Dings_Video_Toggle-Parent"); Player_New.Add_Html_Class("Dings_Video_Toggle-Child"); } // FIXME: Enable Code Highlight_Toc(Element) { // Remove highlight from all items const Item_List = document.getElementById('.Dings_Video_Player-Ul li'); Item_List.forEach(Item => { Item.classList.remove('highlight'); }); // Highlight the selected item const Highlight_Item = Element.parentElement; Highlight_Item.classList.add('highlight'); // Ensure the highlighted item is visible Highlight_Item.scrollIntoView({ behavior: 'smooth', block: 'center' }); } Get_Current_Time() { return this.Player_Video_Current.Get_Current_Time() } Set_Current_Time(Seconds) { console.log("XXX: " + Seconds) this.Player_Video_Current.Set_Current_Time(Seconds); // this.Player_Video_Current.Play() if (this.Player_Audio_Current) { this.Player_Audio_Current.Set_Current_Time(Seconds) // this.Player_Audio_Current.Play() } } }