/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.modules.video.ui; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.olat.core.commons.services.commentAndRating.CommentAndRatingDefaultSecurityCallback; import org.olat.core.commons.services.commentAndRating.CommentAndRatingSecurityCallback; import org.olat.core.commons.services.commentAndRating.ReadOnlyCommentsSecurityCallback; import org.olat.core.commons.services.commentAndRating.ui.UserCommentsAndRatingsController; import org.olat.core.commons.services.image.Size; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.htmlheader.jscss.JSAndCSSComponent; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.gui.render.Renderer; import org.olat.core.gui.render.StringOutput; import org.olat.core.util.CodeHelper; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.filter.FilterFactory; import org.olat.core.util.prefs.Preferences; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSContainerMapper; import org.olat.core.util.vfs.VFSLeaf; import org.olat.modules.video.VideoManager; import org.olat.modules.video.VideoMeta; import org.olat.modules.video.VideoModule; import org.olat.modules.video.VideoTranscoding; import org.olat.modules.video.manager.VideoMediaMapper; import org.olat.repository.RepositoryEntry; import org.olat.repository.handlers.RepositoryHandler; import org.olat.repository.handlers.RepositoryHandlerFactory; import org.springframework.beans.factory.annotation.Autowired; /** * * Initial date: 01.04.2015<br> * @author Dirk Furrer, dirk.furrer@frentix.com, http://www.frentix.com * */ public class VideoDisplayController extends BasicController { private static final String GUIPREF_KEY_PREFERRED_RESOLUTION = "preferredResolution"; @Autowired private VideoModule videoModule; @Autowired private VideoManager videoManager; private UserCommentsAndRatingsController commentsAndRatingCtr; private VelocityContainer mainVC; // User preferred resolution, stored in GUI prefs private Integer userPreferredResolution = null; private final boolean readOnly; private RepositoryEntry entry; private String descriptionText; private String mediaRepoBaseUrl; public VideoDisplayController(UserRequest ureq, WindowControl wControl, RepositoryEntry entry, boolean autoWidth) { this(ureq, wControl, entry, false, false, false, true, null, false, autoWidth, null, false); } public VideoDisplayController(UserRequest ureq, WindowControl wControl, RepositoryEntry entry) { this(ureq, wControl, entry, false, false, false, true, null, false, false, null, false); } public VideoDisplayController(UserRequest ureq, WindowControl wControl, RepositoryEntry entry, Boolean autoplay, Boolean showComments, Boolean showRating, Boolean showTitleAndDescription, String OresSubPath, boolean customDescription, boolean autoWidth, String descriptionText, boolean readOnly) { super(ureq, wControl); this.entry = entry; this.readOnly = readOnly; this.descriptionText = (customDescription ? this.descriptionText = descriptionText : null); mainVC = createVelocityContainer("video_run"); putInitialPanel(mainVC); RepositoryHandler handler = RepositoryHandlerFactory.getInstance().getRepositoryHandler(entry); VFSContainer mediaContainer = handler.getMediaContainer(entry); if(mediaContainer != null) { mediaRepoBaseUrl = registerMapper(ureq, new VFSContainerMapper(mediaContainer.getParentContainer())); } // load mediaelementjs player and sourcechooser plugin StringOutput sb = new StringOutput(50); Renderer.renderStaticURI(sb, "movie/mediaelementjs/mediaelementplayer.min.css"); String[] cssPath = new String[] {sb.toString()}; String[] jsCodePath= new String[] { "movie/mediaelementjs/mediaelement-and-player.min.js", "movie/mediaelementjs/features/oo-mep-feature-sourcechooser.js" }; JSAndCSSComponent mediaelementjs = new JSAndCSSComponent("mediaelementjs", jsCodePath ,cssPath); mainVC.put("mediaelementjs", mediaelementjs); VFSLeaf video = videoManager.getMasterVideoFile(entry.getOlatResource()); if(video != null) { VideoMeta videoMetadata = videoManager.getVideoMetadata(entry.getOlatResource()); if(autoWidth){ mainVC.contextPut("height", 480); mainVC.contextPut("width", "100%"); } else if(videoMetadata != null) { mainVC.contextPut("height", videoMetadata.getHeight()); mainVC.contextPut("width", videoMetadata.getWidth()); } else { mainVC.contextPut("height", 480); mainVC.contextPut("width", 640); } // Load users preferred version from GUI prefs Preferences guiPrefs = ureq.getUserSession().getGuiPreferences(); userPreferredResolution = (Integer) guiPrefs.get(VideoDisplayController.class, GUIPREF_KEY_PREFERRED_RESOLUTION); if (userPreferredResolution == null) { userPreferredResolution = videoModule.getPreferredDefaultResolution(); } mainVC.contextPut("autoplay", autoplay); if ((showComments || showRating) && !ureq.getUserSession().getRoles().isGuestOnly()) { CommentAndRatingSecurityCallback ratingSecCallback = readOnly ? new ReadOnlyCommentsSecurityCallback() : new CommentAndRatingDefaultSecurityCallback(getIdentity(), false, false); commentsAndRatingCtr = new UserCommentsAndRatingsController(ureq, getWindowControl(),entry.getOlatResource(), OresSubPath , ratingSecCallback,showComments, showRating, true); if (showComments) { commentsAndRatingCtr.expandComments(ureq); } listenTo(commentsAndRatingCtr); mainVC.put("commentsAndRating", commentsAndRatingCtr.getInitialComponent()); } mainVC.contextPut("showTitleAndDescription", showTitleAndDescription); // Finally load the video, transcoded versions and tracks loadVideo(ureq, video); } } /** * Reload the video, e.g. when new captions or transcoded versions are available * @param ureq */ protected void reloadVideo(UserRequest ureq) { //load video as VFSLeaf VFSLeaf video = videoManager.getMasterVideoFile(entry.getOlatResource()); loadVideo(ureq, video); mainVC.contextPut("addForceReload", "?t=" + CodeHelper.getRAMUniqueID()); } /** * Reload video poster when the video poster has been exchanged */ protected void reloadVideoPoster() { // Check for null-value posters VFSLeaf poster = videoManager.getPosterframe(entry.getOlatResource()); mainVC.contextPut("usePoster", Boolean.valueOf(poster != null && poster.getSize() > 0)); // avoid browser caching of poster resource mainVC.contextPut("nocache", "?t=" + CodeHelper.getRAMUniqueID()); } /** * Set the text with url rewrite for embedded images, latex... * @param text * @param key */ private void setText(String text, String key) { if(StringHelper.containsNonWhitespace(text)) { text = StringHelper.xssScan(text); if(mediaRepoBaseUrl != null) { text = FilterFactory.getBaseURLToMediaRelativeURLFilter(mediaRepoBaseUrl).filter(text); } text = Formatter.formatLatexFormulas(text); } mainVC.contextPut(key, text); } /** * Internal helper to do the actual video loading, checking for transcoded versions and captions * @param ureq * @param video */ private void loadVideo(UserRequest ureq, VFSLeaf video) { mainVC.contextPut("title", entry.getDisplayname()); String desc = (descriptionText != null ? descriptionText : entry.getDescription()); setText(desc, "description"); String authors = entry.getAuthors(); mainVC.contextPut("authors", (StringHelper.containsNonWhitespace(authors) ? authors : null)); if(video != null) { // get resolution of master video resource Size masterResolution = videoManager.getVideoResolutionFromOLATResource(entry.getOlatResource()); String masterTitle = videoManager.getDisplayTitleForResolution(masterResolution.getHeight(), getTranslator()); String masterSize = " (" + Formatter.formatBytes(videoManager.getVideoMetadata(entry.getOlatResource()).getSize()) + ")"; boolean addMaster = true; // Mapper for Video String masterMapperId = "master-" + entry.getOlatResource().getResourceableId(); String masterUrl = registerCacheableMapper(ureq, masterMapperId, new VideoMediaMapper(videoManager.getMasterContainer(entry.getOlatResource()))); mainVC.contextPut("masterUrl", masterUrl); // Mapper for versions specific because not in same base as the resource itself String transcodingMapperId = "transcoding-" + entry.getOlatResource().getResourceableId(); VFSContainer transcodedContainer = videoManager.getTranscodingContainer(entry.getOlatResource()); String transcodedUrl = registerCacheableMapper(ureq, transcodingMapperId, new VideoMediaMapper(transcodedContainer)); mainVC.contextPut("transcodedUrl", transcodedUrl); // Add transcoded versions List<VideoTranscoding> videos = videoManager.getVideoTranscodings(entry.getOlatResource()); List<VideoTranscoding> readyToPlayVideos = new ArrayList<>(); List<String> displayTitles = new ArrayList<>(); int preferredAvailableResolution = 0; for (VideoTranscoding videoTranscoding : videos) { if (videoTranscoding.getStatus() == VideoTranscoding.TRANSCODING_STATUS_DONE) { readyToPlayVideos.add(videoTranscoding); // Check if at least one has equal height, else use master as resource addMaster &= videoTranscoding.getHeight() < masterResolution.getHeight(); // Use the users preferred resolution or the next higher resolution if (videoTranscoding.getResolution() >= userPreferredResolution.intValue()) { preferredAvailableResolution = readyToPlayVideos.size() - 1; } // Calculate title. Standard title for standard resolution, original title if not standard resolution String title = videoManager.getDisplayTitleForResolution(videoTranscoding.getResolution(), getTranslator()); displayTitles.add(title); } } mainVC.contextPut("addMaster", addMaster); mainVC.contextPut("masterTitle", masterTitle + masterSize); mainVC.contextPut("videos", readyToPlayVideos); mainVC.contextPut("displayTitles", displayTitles); mainVC.contextPut("useSourceChooser", Boolean.valueOf(readyToPlayVideos.size() > 1)); mainVC.contextPut(GUIPREF_KEY_PREFERRED_RESOLUTION, preferredAvailableResolution); // Check for null-value posters VFSLeaf poster = videoManager.getPosterframe(entry.getOlatResource()); mainVC.contextPut("usePoster", Boolean.valueOf(poster != null && poster.getSize() > 0)); // Load the track from config Map<String, String> trackfiles = new HashMap<String, String>(); Map<String, VFSLeaf> configTracks = videoManager.getAllTracks(entry.getOlatResource()); for (HashMap.Entry<String, VFSLeaf> track : configTracks.entrySet()) { trackfiles.put(track.getKey(), track.getValue().getName()); } mainVC.contextPut("trackfiles",trackfiles); // Load video chapter if available mainVC.contextPut("hasChapters", videoManager.hasChapters(entry.getOlatResource())); } } @Override protected void doDispose() { } @Override protected void event(UserRequest ureq, Component source, Event event) { if(source == mainVC){ String cmd = event.getCommand(); if (StringHelper.containsNonWhitespace(cmd)) { String currentTime = ureq.getHttpReq().getParameter("currentTime"); String duration = ureq.getHttpReq().getParameter("duration"); String src = ureq.getHttpReq().getParameter("src"); logDebug(cmd + " " + currentTime + " " + duration + " " + src, null); switch(cmd) { case "play": fireEvent(ureq, new VideoEvent(VideoEvent.PLAY, currentTime, duration)); break; case "pause": fireEvent(ureq, new VideoEvent(VideoEvent.PAUSE, currentTime, duration)); break; case "seeked": fireEvent(ureq, new VideoEvent(VideoEvent.SEEKED, currentTime, duration)); break; case "ended": fireEvent(ureq, new VideoEvent(VideoEvent.ENDED, currentTime, duration)); break; } updateGUIPreferences(ureq, src); } } } /** * Update the users preferred resolution in the GUI prefs from the given video URL * @param ureq * @param src */ private void updateGUIPreferences(UserRequest ureq, String src) { if (src != null) { int start = src.lastIndexOf("/"); if (start != -1) { String video = src.substring(start + 1); int end = video.indexOf("video"); if (end > 0) { // dont's save master videos String resolution = video.substring(0, end); try { int res = Integer.parseInt(resolution.trim()); if (userPreferredResolution == null || userPreferredResolution.intValue() != res) { // update GUI prefs, reload first Preferences guiPrefs = ureq.getUserSession().getGuiPreferences(); userPreferredResolution = (Integer) guiPrefs.get(VideoDisplayController.class, GUIPREF_KEY_PREFERRED_RESOLUTION); guiPrefs.putAndSave(VideoDisplayController.class, GUIPREF_KEY_PREFERRED_RESOLUTION, Integer.valueOf(res)); } } catch (NumberFormatException e) { // ignore, do nothing logDebug("Error parsing the users preferred resolution from url::" + src, null); } } } } } }