UNPKG

globular-mvc

Version:

Generic template to create web-application that made use of globular as backend and materialize as css (wrap in web-component's)

1,323 lines (1,083 loc) 47.9 kB
import { generatePeerToken, getUrl, Model } from '../Model'; import { Application } from "../Application"; import Plyr from 'plyr'; import "./plyr.css" import Hls from "hls.js"; import { ApplicationView } from "../ApplicationView"; import { GetFileTitlesRequest, GetFileVideosRequest, GetTitleFilesRequest } from "globular-web-client/title/title_pb"; import { setMoveable } from './moveable' import { setResizeable } from './rezieable' import { File } from "../File" import { fireResize, formatBoolean, randomUUID } from "./utility"; import { PlayList } from "./Playlist" import { readDir } from "globular-web-client/api"; import { setMinimizeable } from "./minimizeable" import { timeSince } from './BlogPost'; import "../style.css" Object.defineProperty(HTMLMediaElement.prototype, 'playing', { get: function () { return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2); } }) String.prototype.endsWith = function (suffix) { return this.indexOf(suffix, this.length - suffix.length) !== -1; }; // get files associated with the titles, audios or videos... function getTitleFiles(id, indexPath, globule, callback) { let rqst = new GetTitleFilesRequest rqst.setTitleid(id) rqst.setIndexpath(indexPath) generatePeerToken(globule, token => { globule.titleService.getTitleFiles(rqst, { application: Application.application, domain: Application.domain, token: token }) .then(rsp => { callback(rsp.getFilepathsList()) }).catch(err => { callback([]) }) }) } export function playVideos(videos, name) { let videos_ = [...new Map(videos.map(v => [v.getId(), v])).values()] ApplicationView.wait("loading videos playlist...") // here I will get the audi let video_playList = "#EXTM3U\n" video_playList += "#PLAYLIST: " + name + "\n\n" // Generate the playlist with found audio items. let generateVideoPlaylist = () => { let video = videos_.pop(); let globule = video.globule; // set the audio info let indexPath = globule.config.DataPath + "/search/videos" // get the title file path... getTitleFiles(video.getId(), indexPath, globule, files => { if (files.length > 0) { video_playList += `#EXTINF:${video.getDuration()}, ${video.getDescription()}, tvg-id="${video.getId()}"\n` let url = getUrl(globule) if (!files[0].endsWith(".mp4")) { files[0] += "/playlist.m3u8" } let path = files[0].split("/") path.forEach(item => { item = item.trim() if (item.length > 0) url += "/" + encodeURIComponent(item) //* fail to parse if the item is encoded... }) video_playList += url + "\n\n" } if (videos_.length > 0) { generateVideoPlaylist() } else { playVideo(video_playList, null, null, null, globule) } }) } generateVideoPlaylist() } /** * Function to play a video on the same player. * @param {*} path * @param {*} onplay * @param {*} onclose */ export function playVideo(path, onplay, onclose, title, globule) { if (title) { if (title.globule) { globule = title.globule } else if (globule != null) { title.globule = globule } } let menus = document.body.querySelectorAll("globular-dropdown-menu") for (var i = 0; i < menus.length; i++) { menus[i].close() if (menus[i].classList.contains("file-dropdown-menu")) { menus[i].parentNode.removeChild(menus[i]) } } let videoPlayer = document.getElementById("video-player-x") if (videoPlayer == null) { videoPlayer = new VideoPlayer() videoPlayer.id = "video-player-x" } else { videoPlayer.stop() } videoPlayer.hide() videoPlayer.resume = false; videoPlayer.style.zIndex = 100 if (onplay && !videoPlayer.onplay) { videoPlayer.onplay = onplay } // keep the title videoPlayer.titleInfo = title; videoPlayer.globule = globule; if (onclose && !videoPlayer.onclose) { videoPlayer.onclose = onclose } // clear the playlist... if (videoPlayer.playlist) videoPlayer.playlist.clear() // play a given title. if (path.endsWith("video.m3u") || path.startsWith("#EXTM3U")) { videoPlayer.loadPlaylist(path, globule) } else { // make sure the player is not show before the video is loaded. ApplicationView.wait("loading video...") videoPlayer.play(path, globule) } if (!videoPlayer.isMinimized) { ApplicationView.layout.workspace().appendChild(videoPlayer) } return videoPlayer } function getSubtitlesFiles(globule, path, callback) { let subtitlesPath = path.substr(0, path.lastIndexOf(".")) subtitlesPath = subtitlesPath.substring(0, subtitlesPath.lastIndexOf("/") + 1) + ".hidden" + subtitlesPath.substring(subtitlesPath.lastIndexOf("/")) + "/__subtitles__" File.readDir(subtitlesPath, false, callback, err => console.log(err), globule) } function getThumbnailFiles(globule, path, callback) { let subtitlesPath = path.substr(0, path.lastIndexOf(".")) subtitlesPath = subtitlesPath.substring(0, subtitlesPath.lastIndexOf("/") + 1) + ".hidden" + subtitlesPath.substring(subtitlesPath.lastIndexOf("/")) + "/__thumbnail__" File.readDir(subtitlesPath, false, callback, err => console.log(err), globule) } /** * Sample empty component */ export class VideoPlayer extends HTMLElement { // attributes. // Create the applicaiton view. constructor() { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); let hideheader = this.getAttribute("hideheader") != undefined this.titleInfo = null; // movie, serie title, video this.playlist = null; // The playlist... this.globule = null; this.skipPresiousBtn = null; this.stopBtn = null; this.skipNextBtn = null; this.loopBtn = null; this.shuffleBtn = null; this.trackInfo = null; this.loop = true; this.shuffle = false; this.resume = false; this.isMinimized = false; this.onMinimize = null; // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> #container{ /* border-left: 1px solid var(--palette-divider); border-right: 1px solid var(--palette-divider) rgba(255, 255, 255, 0.12); border-top: 1px solid var(--palette-divider) rgba(255, 255, 255, 0.12); */ position: relative; width: 720px; user-select: none; background-color: black; } #content{ position: relative; display: flex; background-color: black; justify-items: center; background-color: black; color: var(--palette-text-primary); } .header{ display: flex; align-items: center; color: var(--palette-text-accent); background-color: var(--palette-primary-accent); border-top: 1px solid var(--palette-action-disabled); border-left: 1px solid var(--palette-action-disabled); } .header span{ flex-grow: 1; text-align: center; font-size: 1.1rem; font-weight: 500; display: inline-block; white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis; } .header paper-icon-button { min-width: 40px; } .header select { background: var(--palette-background-default); color: var(--palette-text-accent); border:0px; outline:0px; } .header paper-icon-button { min-width: 40px; } video { display: block; width:100%; position: "absolute"; top: 0; left: 0; bottom: 0; right: 0; background-color: black; } @media (max-width: 600px) { #container{ width: 100vw; } #content{ flex-direction: column-reverse; } globular-playlist { min-width: 450px; } } @media (min-width: 600px) { globular-playlist { min-width: 450px; } } paper-card { background: black; } </style> <paper-card id="container"> <div class="header" style="${hideheader ? "display:none;" : ""}"> <paper-icon-button id="video-close-btn" icon="icons:close" style="min-width: 40px; --iron-icon-fill-color: var(--palette-text-accent);"></paper-icon-button> <span id="title-span"></span> <select id="audio-track-selector" style="display: none"></select> <paper-icon-button id="title-info-button" icon="icons:arrow-drop-down-circle"></paper-icon-button> </div> <div id="content"> <globular-playlist style="display: none; overflow:hidden; height: 600px;"></globular-playlist> <slot></slot> </div> </paper-card> ` this.container = this.shadowRoot.querySelector("#container") this.container.onclick = (evt) => { evt.stopPropagation() // not interfere with plyr click event... do not remove this line. } this.content = this.shadowRoot.querySelector("#content") this.header = this.shadowRoot.querySelector(".header") this.shadowRoot.querySelector("#title-info-button").onclick = (evt) => { evt.stopPropagation() if (this.titleInfo) { if (this.titleInfo.clearActorsList != undefined) { this.showTitleInfo(this.titleInfo) } else { this.showVideoInfo(this.titleInfo) } } else { ApplicationView.displayMessage("no title information found", 3000) } } // give the focus to the input. this.video = document.createElement("video") this.video.id = "player" this.video.autoplay = true this.video.controls = true this.video.playsinline = true this.onclose = null this.onplay = null let offsetTop = this.header.offsetHeight if (offsetTop == 0) { offsetTop = 60 } this.path = "" this.appendChild(this.video) this.container.style.height = "auto" this.container.name = "video_player" if (localStorage.getItem("__video_player_dimension__")) { let dimension = JSON.parse(localStorage.getItem("__video_player_dimension__")) if (!dimension) { dimension = { with: 600, height: 400 } } // be sure the dimension is no zeros... if (dimension.width < 600) { dimension.width = 600 } this.container.style.width = dimension.width + "px" localStorage.setItem("__notification_editor_dimension__", JSON.stringify({ width: dimension.width, height: dimension.height })) } else { this.container.style.width = "600px" localStorage.setItem("__notification_editor_dimension__", JSON.stringify({ width: 600, height: 400 })) } setResizeable(this.container, (width, height) => { localStorage.setItem("__video_player_dimension__", JSON.stringify({ width: width, height: height })) this.container.style.height = "auto" }) this.container.resizeHeightDiv.style.display = "none" // Set the video to full screen when orientation change. window.addEventListener("orientationchange", (event) => { var orientation = (screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation; if (["landscape-primary", "landscape-secondary"].indexOf(orientation) != -1) { this.becomeFullscreen(); } else if (orientation === undefined) { console.log("The orientation API isn't supported in this browser :("); } }); // set the initial size of the video player to fit the played video... this.video.onplaying = (evt) => { ApplicationView.resume() if (this.resume) { this.show() return } this.resume = true let w = ApplicationView.layout.width(); if (w < 500) { if (!this.minimize) this.container.style.width = "100vw" } else { if (this.video.videoHeight > 0 && this.video.videoWidth > 0) { let height = this.video.videoHeight let maxWidth = this.video.videoWidth if (maxWidth > screen.width) { maxWidth = screen.width height = maxWidth * (this.video.videoHeight / this.video.videoWidth) } if (this.video.videoHeight > screen.height - 250) { height = screen.height - 250 maxWidth = height * (this.video.videoWidth / this.video.videoHeight) } if (this.playlist.style.display != "none") { if (maxWidth + this.playlist.offsetWidth < screen.width) { this.playlist.__width__ = this.playlist.offsetWidth maxWidth += this.playlist.offsetWidth } } else if (this.playlist.count() > 1) { maxWidth += this.playlist.__width__ } this.container.maxWidth = maxWidth this.container.style.height = "auto" // event resize the video only if the video is new... this.playlist.style.height = height + "px" this.container.style.width = maxWidth + "px" localStorage.setItem("__video_player_dimension__", JSON.stringify({ width: maxWidth, height: height })) } } this.show() } // toggle full screen when the user double click on the header. this.header.ondblclick = () => { var type = this.player.media.tagName.toLowerCase(), toggle = document.querySelector("[data-plyr='fullscreen']"); if (type === "video" && toggle) { toggle.addEventListener("click", this.player.toggleFullscreen, false); } toggle.click() } setMoveable(this.shadowRoot.querySelector("#title-span"), this.container, (left, top) => { /** */ }, this, offsetTop) setMinimizeable(this.header, this, "video_player", "Video", "icons:theaters") // Plyr give a nice visual to the video player. // TODO set the preview and maybe quality bitrate if possible... // So here I will get the vtt file if one exist... this.player = new Plyr(this.video, { captions: { active: true, update: true,// THAT line solved my problem } }); this.shadowRoot.querySelector("#video-close-btn").onclick = () => { this.close() } // https://www.tomsguide.com/how-to/how-to-set-chrome-flags // you must set enable-experimental-web-platform-features to true // chrome://flags/ this.video.onloadeddata = () => { ApplicationView.resume() // Options for the observer (which mutations to observe) var config = { attributes: true, subtree: true }; getSubtitlesFiles(this.globule, this.path, subtitles_files => { let globule = this.globule let url = getUrl(globule) subtitles_files.files.forEach(f => { let track = document.createElement("track") // <track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default /> track.kind = "captions" // ex. View_From_A_Blue_Moon_Trailer-576p.fr.vtt let language_id = f.name.split(".")[f.name.split.length - 1] const languageNames = new Intl.DisplayNames([language_id], { type: 'language' }); track.label = languageNames.of(language_id)// todo set the true language. let url_ = f.path url_ = f.path if (url_.startsWith("/")) { url_ = url + url_ } else { url_ = url + "/" + url_ } track.src = url_ track.srclang = language_id this.player.media.appendChild(track) }) }) if (this.video.audioTracks) { // This will set the video langual... if (this.video.audioTracks.length > 1) { let audioTrackSelect = this.shadowRoot.querySelector("#audio-track-selector") audioTrackSelect.style.display = "block" for (let i = 0; i < this.video.audioTracks.length; i++) { let track = this.video.audioTracks[i] let option = document.createElement("option") option.innerHTML = track.language option.value = i audioTrackSelect.appendChild(option) } // Set the language with the browser let browser_language = navigator.language || navigator.userLanguage; // IE <= 10 for (let i = 0; i < this.video.audioTracks.length; i++) { let track_language = this.video.audioTracks[i].language.substr(0, 2); // +++ Set the enabled audio track language +++ if (track_language) { // When the track language matches the browser language, then enable that audio track if (track_language === browser_language) { // When one audio track is enabled, others are automatically disabled this.video.audioTracks[i].enabled = true; audioTrackSelect.value = i this.player.rewind(0) } else { this.video.audioTracks[i].enabled = false; } } } audioTrackSelect.onchange = (evt) => { evt.stopPropagation() if (this.player) { var selectElement = evt.target; var value = selectElement.value; for (let i = 0; i < this.video.audioTracks.length; i++) { let track = this.video.audioTracks[i] if (i == value) { track.enabled = true this.player.forward(0) } else { track.enabled = false } } } } // Options for the observer (which mutations to observe) var config = { attributes: true, subtree: true }; } } } // HLS for streamming... this.hls = null; this.playlist = this.shadowRoot.querySelector("globular-playlist") this.playlist.videoPlayer = this } connectedCallback() { if (this.skipPresiousBtn) { return } // hide plyr function not us let items = this.querySelectorAll(".plyr__controls__item") for (var i = 0; i < items.length; i++) { if (items[i].getAttribute("data-plyr") == "pip") { items[i].style.display = "none" } } let controls = this.querySelector(".plyr__controls") controls.style.flexWrap = "wrap" controls.style.justifyContent = "flex-start" let plyrVideo = this.querySelector(".plyr--video") plyrVideo.style.backgroundColor = "black" // add additional button for the playlist... let html = ` <div style="flex-basis: 100%; height: 5px;"></div> <iron-icon style="--iron-icon-height: 32px; --iron-icon-width: 32px; fill: #424242;" class="plyr__controls__item plyr__control" title="Shuffle Playlist" id="shuffle" icon="av:shuffle"></iron-icon> <iron-icon style="--iron-icon-height: 32px; --iron-icon-width: 32px;" class="plyr__controls__item plyr__control" id="skip-previous" title="Previous Track" icon="av:skip-previous"></iron-icon> <iron-icon style="--iron-icon-height: 32px; --iron-icon-width: 32px;" class="plyr__controls__item plyr__control" id="skip-next" title="Next Track" icon="av:skip-next"></iron-icon> <iron-icon style="--iron-icon-height: 32px; --iron-icon-width: 32px;" class="plyr__controls__item plyr__control" id="stop" title="Stop" icon="av:stop"></iron-icon> <iron-icon style="--iron-icon-height: 32px; --iron-icon-width: 32px;" class="plyr__controls__item plyr__control" title="Loop Playlist" id="repeat" icon="av:repeat"></iron-icon> <div id="track-info"></div> ` let range = document.createRange() controls.appendChild(range.createContextualFragment(html)) // Now the buttons actions. this.skipPresiousBtn = this.querySelector("#skip-previous") this.stopBtn = this.querySelector("#stop") this.skipNextBtn = this.querySelector("#skip-next") this.loopBtn = this.querySelector("#repeat") this.shuffleBtn = this.querySelector("#shuffle") this.trackInfo = this.querySelector("#track-info") if (this.playlist.count() <= 1) { this.hidePlaylist() } let playPauseBtn = controls.children[0] playPauseBtn.addEventListener("click", evt => { let state = evt.target.getAttribute("aria-label") if (state == "Play") { this.playlist.resumePlaying() } else if (state == "Pause") { this.playlist.pausePlaying() } }, true) plyrVideo.addEventListener("click", evt => { let state = playPauseBtn.getAttribute("aria-label") if (state == "Play") { this.playlist.resumePlaying() } else if (state == "Pause") { this.playlist.pausePlaying() } }, true) this.loop = false if (localStorage.getItem("video_loop")) { this.loop = localStorage.getItem("video_loop") == "true" } if (this.loop) { this.loopBtn.style.fill = "white" } else { this.loopBtn.style.fill = "gray" } this.shuffle = false if (localStorage.getItem("video_shuffle")) { this.shuffle = localStorage.getItem("video_shuffle") == "true" } if (this.shuffle) { this.shuffleBtn.style.fill = "white" } else { this.shuffleBtn.style.fill = "#424242" } // stop the audio player.... this.stopBtn.onclick = () => { this.stop() if (this.playlist) { this.playlist.stop() } this.trackInfo.innerHTML = "" } this.skipNextBtn.onclick = () => { this.stop() if (this.playlist) { this.playlist.playNext() } } this.skipPresiousBtn.onclick = () => { this.stop() if (this.playlist) { this.playlist.playPrevious() } } // loop... this.loopBtn.onclick = () => { if (this.loop) { localStorage.setItem("video_loop", "false"); this.loop = false; } else { localStorage.setItem("video_loop", "true") this.loop = true; } if (this.loop) { this.loopBtn.style.fill = "white" } else { this.loopBtn.style.fill = "#424242" } } this.shuffleBtn.onclick = () => { if (this.shuffle) { localStorage.setItem("video_shuffle", "false"); this.shuffle = false; } else { localStorage.setItem("video_shuffle", "true") this.shuffle = true; } if (this.shuffle) { this.shuffleBtn.style.fill = "white" } else { this.shuffleBtn.style.fill = "#424242" } this.playlist.orderItems() } } loadPlaylist(path, globule) { this.playlist.clear() this.playlist.load(path, globule, this, () => { // show playlist after loading it... (hide it if number of item is less than one) this.showPlaylist() }) // set the css value to display the playlist correctly... window.addEventListener("resize", (evt) => { if (this.isMinimized) { this.content.style.height = "auto" return } let w = ApplicationView.layout.width(); if (w < 500) { this.content.style.height = "calc(100vh - 100px)" this.content.style.overflowY = "auto" } else { this.content.style.height = "" this.content.style.overflowY = "" if (this.video.videoHeight < this.content.offsetHeight) { this.playlist.style.height = this.video.videoHeight + "px" } else { this.playlist.style.height = this.content.offsetHeight + "px" } } }) setTimeout(fireResize(), 500) } showPlaylist() { if (this.playlist.count() > 1) { this.playlist.style.display = "block" let playlistButtons = this.querySelectorAll("iron-icon") for (var i = 0; i < playlistButtons.length; i++) { playlistButtons[i].style.display = "block" } } else { this.hidePlaylist() } } hidePlaylist() { this.playlist.style.display = "none" this.shuffleBtn.style.display = "none" this.skipNextBtn.style.display = "none" this.skipPresiousBtn.style.display = "none" this.stopBtn.style.display = "none" this.loopBtn.style.display = "none" this.trackInfo.style.display = "none" } setTarckInfo(index, total) { // display the position on the list... console.log("set " + index + " of " + total) } showVideoInfo(video) { let uuid = randomUUID() let html = ` <paper-card id="video-info-box-dialog-${uuid}" style="background: var(--palette-background-default); "> <globular-informations-manager id="video-info-box"></globular-informations-manager> </paper-card> ` let videoInfoBox = document.getElementById("video-info-box") if (videoInfoBox == undefined) { let range = document.createRange() document.body.appendChild(range.createContextualFragment(html)) videoInfoBox = document.getElementById("video-info-box") let parent = videoInfoBox.parentNode if (parent) { parent.style.position = "fixed" parent.style.top = "75px" parent.style.left = "50%" parent.style.transform = "translate(-50%)" videoInfoBox.onclose = () => { parent.parentNode.removeChild(parent) } } } videoInfoBox.setVideosInformation([video]) } showTitleInfo(title) { let uuid = randomUUID() let html = ` <paper-card id="video-info-box-dialog-${uuid}" style="background-color: var(--palette-background-default);"> <globular-informations-manager id="title-info-box"></globular-informations-manager> </paper-card> ` let titleInfoBox = document.getElementById("title-info-box") if (titleInfoBox == undefined) { let range = document.createRange() document.body.appendChild(range.createContextualFragment(html)) titleInfoBox = document.getElementById("title-info-box") let parent = document.getElementById("video-info-box-dialog-" + uuid) parent.style.position = "fixed" parent.style.top = "75px" parent.style.left = "50%" parent.style.transform = "translate(-50%)" titleInfoBox.onclose = () => { parent.parentNode.removeChild(parent) } } titleInfoBox.setTitlesInformation([title]) } play(path, globule, titleInfo) { if (this.isMinimized) { this.minimize() } if (titleInfo) { this.titleInfo = titleInfo } generatePeerToken(globule, token => { let url = path; if (!url.startsWith("http")) { url = getUrl(globule) path.split("/").forEach(item => { item = item.trim() if (item.length > 0) { url += "/" + encodeURIComponent(item) } }) url += "?application=" + Model.application url += "&token=" + token } else { var parser = document.createElement('a'); url += "?application=" + Model.application url += "&token=" + token parser.href = url path = decodeURIComponent(parser.pathname) } if (this.path == path) { this.resume = true; this.video.play() return } else { // keep track of the current path this.path = path this.resume = false; } // validate url access. fetch(url, { method: "HEAD" }) .then((response) => { if (response.status == 401) { ApplicationView.resume() ApplicationView.displayMessage(`unable to read the file ${path} Check your access privilege`, 3500) this.close() return } else if (response.status == 200) { if (File.hasLocal) { File.hasLocal(path, exists => { if (exists) { // local-media this.play_(path, globule, true, token) } else { this.play_(path, globule, false, token) } }) } else { this.play_(path, globule, false, token) } } else { throw new Error(response.status) } }) .catch((error) => { ApplicationView.resume() ApplicationView.displayMessage("fail to read url " + url + "with error " + error, 4000) if (this.parentNode) { this.parentNode.removeChild(this) } }); }, err => ApplicationView.displayMessage(err, 4000)) } play_(path, globule, local = false, token) { this.hide() // replace separator... path = path.split("\\").join("/") this.style.zIndex = 100 // Set the title... let thumbnailPath = path.replace("/playlist.m3u8", "") this.shadowRoot.querySelector("#title-span").innerHTML = thumbnailPath.substring(thumbnailPath.lastIndexOf("/") + 1) this.shadowRoot.querySelector("#container").style.display = "" // So Here I will try to get the title or the video info... if (!globule) { globule = Model.globular } // Now I will test if imdb info are allready asscociated. let getTitleInfo = (path, callback) => { // The title info is already set... if (this.titleInfo) { if (this.titleInfo.getName != undefined) { this.titleInfo.isVideo = false; callback([this.titleInfo]) return } else { this.titleInfo.isVideo = true; callback([]) return } } let rqst = new GetFileTitlesRequest rqst.setIndexpath(globule.config.DataPath + "/search/titles") rqst.setFilepath(path) globule.titleService.getFileTitles(rqst, { application: Application.application, domain: Application.domain, token: token }) .then(rsp => { rsp.getTitles().getTitlesList().forEach(t => t.globule = globule) callback(rsp.getTitles().getTitlesList()) }) } let getVideoInfo = (path, callback) => { if (this.titleInfo) { if (this.titleInfo.getDescription != undefined) { this.titleInfo.isVideo = true; callback([this.titleInfo]) return } else { this.titleInfo.isVideo = false; callback([]) return } } let rqst = new GetFileVideosRequest rqst.setIndexpath(globule.config.DataPath + "/search/videos") rqst.setFilepath(path) globule.titleService.getFileVideos(rqst, { application: Application.application, domain: Application.domain, token: token }) .then(rsp => { rsp.getVideos().getVideosList().forEach(v => v.globule = globule) callback(rsp.getVideos().getVideosList()) }) } getVideoInfo(path, videos => { if (videos.length > 0) { let video = videos.pop() this.titleInfo = video this.titleInfo.isVideo = true this.shadowRoot.querySelector("#title-span").innerHTML = video.getDescription().replace("</br>", " ") // Start where the video was stop last time... TODO if (this.titleInfo) { if (localStorage.getItem(this.titleInfo.getId())) { let currentTime = parseFloat(localStorage.getItem(this.titleInfo.getId())) this.video.currentTime = currentTime } this.video.onended = () => { this.resume = false; if (this.titleInfo) localStorage.removeItem(this.titleInfo.getId()) if (this.playlist.items.length > 1) { this.playlist.playNext() } else if (this.loop) { if (File.hasLocal) { File.hasLocal(this.path, exists => { if (this.titleInfo) { this.play(this.path, this.titleInfo.globule) } else { this.play(this.path) } }) } else { if (this.titleInfo) this.play(this.path, this.titleInfo.globule) else this.play(this.path) } } else { this.stop() } } //Model.eventHub.publish("play_video_player_evt_", { _id: this.titleInfo.getId(), isVideo: this.titleInfo.isVideo, currentTime: this.video.currentTime, date: new Date() }, true) } } }) getTitleInfo(path, tittles => { if (tittles.length > 0) { let title = tittles.pop() this.titleInfo = title this.titleInfo.isVideo = false this.shadowRoot.querySelector("#title-span").innerHTML = title.getName() if (title.getYear()) { this.shadowRoot.querySelector("#title-span").innerHTML += " (" + title.getYear() + ") " } if (title.getType() == "TVEpisode") { this.shadowRoot.querySelector("#title-span").innerHTML += " S" + title.getSeason() + "E" + title.getEpisode() } if (this.onplay != null) { this.onplay(this.player, title) } // Start where the video was stop last time... TODO if (this.titleInfo) { if (localStorage.getItem(this.titleInfo.getId())) { let currentTime = parseFloat(localStorage.getItem(this.titleInfo.getId())) this.video.currentTime = currentTime } Model.eventHub.publish("play_video_player_evt_", { _id: this.titleInfo.getId(), isVideo: this.titleInfo.isVideo, currentTime: this.video.currentTime, domain: this.titleInfo.globule.domain, duration: this.video.duration, date: new Date() }, true) } } }) // Only HLS and MP4 are allow by the video player so if is not one it's the other... if (thumbnailPath.lastIndexOf(".mp4") != -1 || thumbnailPath.lastIndexOf(".MP4") != -1) { thumbnailPath = thumbnailPath.substring(0, thumbnailPath.lastIndexOf(".")) } else if (!path.endsWith("/playlist.m3u8")) { path += "/playlist.m3u8" } else { if (!(path.endsWith("/playlist.m3u8") || path.endsWith(".mp4") || path.endsWith(".webm"))) { ApplicationView.displayMessage("the file cannot be play by the video player", 3000) return } } thumbnailPath = thumbnailPath.substring(0, thumbnailPath.lastIndexOf("/") + 1) + ".hidden" + thumbnailPath.substring(thumbnailPath.lastIndexOf("/")) + "/__timeline__/thumbnails.vtt" // set the complete url. // Get image from the globule. let url = getUrl(globule) if (thumbnailPath.startsWith("/")) { thumbnailPath = url + thumbnailPath } else { thumbnailPath = url + "/" + thumbnailPath } this.player.setPreviewThumbnails({ enabled: "true", src: thumbnailPath }) if (!this.video.paused && this.video.currentSrc.endsWith(path)) { // Do nothing... return } else if (this.video.paused && this.video.currentSrc.endsWith(path)) { // Resume the video... this.video.play() return } path.split("/").forEach(item => { item = item.trim() if (item.length > 0) { url += "/" + encodeURIComponent(item) } }) url += "?application=" + Model.application url += "&token=" + token if (local) { url = "local-media://" + path } // Set the path and play. this.video.src = url if (path.endsWith(".m3u8")) { if (Hls.isSupported()) { this.hls = new Hls( { xhrSetup: xhr => { xhr.setRequestHeader('application', Model.application) xhr.setRequestHeader('token', token) } } ); this.hls.attachMedia(this.video); this.hls.on(Hls.Events.MEDIA_ATTACHED, () => { this.hls.loadSource(url); this.hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => { this.video.play(); }); }); } } } becomeFullscreen() { if (this.video.requestFullscreen) { this.video.requestFullscreen(); } else if (this.video.mozRequestFullScreen) { /* Firefox */ this.video.mozRequestFullScreen(); } else if (this.video.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ this.video.webkitRequestFullscreen(); } else if (this.video.msRequestFullscreen) { /* IE/Edge */ this.video.msRequestFullscreen(); } } /** * Close the player... */ close() { this.hide() this.stop() if (this.parentNode) this.parentElement.removeChild(this) if (this.onclose) { this.onclose() } } /** * Stop the video. */ stop() { this.video.pause(); // keep the current video location if (this.titleInfo != null) { // Stop the video if (this.video.duration != this.video.currentTime) { Model.eventHub.publish("stop_video_player_evt_", { _id: this.titleInfo.getId(), domain: this.titleInfo.globule.domain, isVideo: this.titleInfo.isVideo, currentTime: this.video.currentTime, date: new Date() }, true) } else { Model.eventHub.publish("remove_video_player_evt_", { _id: this.titleInfo.getId(), domain: this.titleInfo.globule.domain, isVideo: this.titleInfo.isVideo, currentTime: this.video.currentTime, date: new Date() }, true) } // keep video info in the local storage... localStorage.setItem(this.titleInfo.getId(), this.video.currentTime) } } hide() { this.hideHeader() this.container.style.display = "none" this.header.style.display = "none"; let plyrVideo = this.querySelector(".plyr--video") plyrVideo.style.display = "none"; } show() { this.container.style.display = "" let plyrVideo = this.querySelector(".plyr--video") plyrVideo.style.display = "" this.showHeader() } hideHeader() { this.header.style.display = "none"; } showHeader() { if (!this.isMinimized) { this.header.style.display = ""; } } setHeight(h) { this.querySelector("video").style.height = h + "px" } resetHeight() { this.querySelector("video").style.height = "" } /** * Minimize the element */ minimize() { if (this.isMinimized) { if (this.onMinimize) { this.onMinimize() } return } this.isMinimized = true this.hidePlaylist() this.header.style.display = "none" // the container this.container.style.__max_width__ = this.container.style.maxWidth this.container.style.maxWidth = "300px" this.container.style.__width__ = this.container.style.width this.container.style.width = "300px" // Fix the container height this.container.style.maxHeight = "300px" this.container.style.height = "auto" this.container.style.position = "initial" if (this.playlist.count() > 1) { this.skipNextBtn.style.display = "" this.skipPresiousBtn.style.display = "" } if (this.onMinimize) { this.onMinimize() } } /** * Maximize (restore to it normal size) the element */ maximize() { if (!this.isMinimized) { return } this.isMinimized = false this.container.style.width = this.container.style.__width__ this.container.style.maxWidth = this.container.maxWidth + "px" this.container.style.position = "fixed" this.container.style.height = "auto" this.container.style.maxHeight = "" this.showHeader() this.showPlaylist() ApplicationView.layout.workspace().appendChild(this) } } customElements.define('globular-video-player', VideoPlayer)