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()
}
}
}