diff --git a/src/main/java/org/olat/core/commons/services/video/MovieService.java b/src/main/java/org/olat/core/commons/services/video/MovieService.java index 88169d176eb5941da408cbd415cfac1eee150c8a..26e809f0596f8fe958eb4454801161a87df79e5a 100644 --- a/src/main/java/org/olat/core/commons/services/video/MovieService.java +++ b/src/main/java/org/olat/core/commons/services/video/MovieService.java @@ -39,15 +39,23 @@ public interface MovieService { */ public Size getSize(VFSLeaf image, String suffix); - /** - * Calculate the duration of the given movie + * Calculate the duration of the given movie. * * @param media * @param suffix * @return long duration in milliseconds */ public long getDuration(VFSLeaf media, String suffix); + + /** + * Calculate the number of frames for the given movie. + * + * @param media + * @param suffix + * @return long duration in milliseconds + */ + public long getFrameCount(VFSLeaf media, String suffix); /** * Checks if a file is really an mp4 file we can handle diff --git a/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java b/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java index 36a935b05e3a4563c47380b85ed7dee1df9f05cb..94c7ce58d3676f09e506f4bfde2e8eed68001917 100644 --- a/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java +++ b/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java @@ -177,6 +177,33 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { return -1; } + + @Override + public long getFrameCount(VFSLeaf media, String suffix) { + File file = null; + if(media instanceof VFSCPNamedItem) { + media = ((VFSCPNamedItem)media).getDelegate(); + } + if(media instanceof LocalFileImpl) { + file = ((LocalFileImpl)media).getBasefile(); + } + if(file == null) { + return -1; + } + + if(extensions.contains(suffix)) { + try(RandomAccessFile accessFile = new RandomAccessFile(file, "r")) { + FileChannel ch = accessFile.getChannel(); + FileChannelWrapper in = new FileChannelWrapper(ch); + MP4Demuxer demuxer1 = new MP4Demuxer(in); + return demuxer1.getVideoTrack().getFrameCount(); + } catch (Exception | AssertionError e) { + log.error("Cannot extract num. of frames of: " + media, e); + } + } + + return -1; + } @Override public boolean isMP4(VFSLeaf media, String fileName) { diff --git a/src/main/java/org/olat/modules/video/VideoManager.java b/src/main/java/org/olat/modules/video/VideoManager.java index 1f50f82fe7b20154a956cacd370471d1eef7790b..ed475cccbe35c9b4c71de9265537d76d3d83a774 100644 --- a/src/main/java/org/olat/modules/video/VideoManager.java +++ b/src/main/java/org/olat/modules/video/VideoManager.java @@ -54,21 +54,21 @@ public interface VideoManager { * @param videoResource the video resource * @return true, if successful */ - public abstract boolean hasVideoFile(OLATResource videoResource); + public boolean hasVideoFile(OLATResource videoResource); /** * get Videofile as File representation * @param videoResource * @return File */ - public abstract File getVideoFile(OLATResource videoResource); + public File getVideoFile(OLATResource videoResource); /** * get actually configured posterframe as VFSLeaf representation * @param videoResource * @return VFSLeaf */ - public abstract VFSLeaf getPosterframe(OLATResource videoResource); + public VFSLeaf getPosterframe(OLATResource videoResource); /** * set posterframe for given videoResource @@ -354,7 +354,9 @@ public interface VideoManager { * @param OLATResource videoResource * @return the video duration */ - public abstract long getVideoDuration(OLATResource videoResource); + public long getVideoDuration(OLATResource videoResource); + + public long getVideoFrameCount(OLATResource videoResource); /** * Gets the video resolution from olat resource. diff --git a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java index 99352c0c15fcbe743ae918dbf102db81c6dcb68a..2297c546703c35ed969e1e668c2afda16049db35 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java +++ b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java @@ -350,7 +350,6 @@ public class VideoManagerImpl implements VideoManager { return videoFile.getBasefile(); } - /** * Resolve the given path to a file in the master directory and return it * @@ -889,17 +888,22 @@ public class VideoManagerImpl implements VideoManager { } @Override - public long getVideoDuration (OLATResource videoResource){ + public long getVideoDuration(OLATResource videoResource){ VFSContainer masterContainer = getMasterContainer(videoResource); - VFSLeaf video = (VFSLeaf) masterContainer.resolve(FILENAME_VIDEO_MP4); - long duration = movieService.getDuration(video, FILETYPE_MP4); - return duration; + VFSLeaf video = (VFSLeaf)masterContainer.resolve(FILENAME_VIDEO_MP4); + return movieService.getDuration(video, FILETYPE_MP4); } + @Override + public long getVideoFrameCount(OLATResource videoResource) { + VFSContainer masterContainer = getMasterContainer(videoResource); + VFSLeaf video = (VFSLeaf)masterContainer.resolve(FILENAME_VIDEO_MP4); + return movieService.getFrameCount(video, FILETYPE_MP4); + } + @Override public List<VideoMetaImpl> getAllVideoResourcesMetadata() { - List<VideoMetaImpl> metadata = videoMetadataDao.getAllVideoResourcesMetadata(); - return metadata; + return videoMetadataDao.getAllVideoResourcesMetadata(); } @Override diff --git a/src/main/java/org/olat/modules/video/ui/VideoPosterEditController.java b/src/main/java/org/olat/modules/video/ui/VideoPosterEditController.java index 28ffd1bbff024779a610500209db47bcd74c7d1f..d1e5f9d12f52518cda36e563ed19e44d35434c46 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoPosterEditController.java +++ b/src/main/java/org/olat/modules/video/ui/VideoPosterEditController.java @@ -49,7 +49,7 @@ public class VideoPosterEditController extends FormBasicController { @Autowired private VideoManager videoManager; - private VFSLeaf posterFile; + private OLATResource videoResource; private FormLayoutContainer displayContainer; private FormLink replaceImage; @@ -73,7 +73,6 @@ public class VideoPosterEditController extends FormBasicController { displayContainer.contextPut("hint", translate("video.config.poster.hint")); - posterFile = videoManager.getPosterframe(videoResource); updatePosterImage(ureq, videoResource); displayContainer.setLabel("video.config.poster", null); formLayout.add(displayContainer); @@ -111,59 +110,65 @@ public class VideoPosterEditController extends FormBasicController { public void event(UserRequest ureq, Controller source, Event event) { if(source == posterUploadForm || source == posterSelectionForm){ if(event instanceof FolderEvent){ - posterFile = (VFSLeaf) ((FolderEvent) event).getItem(); - flc.setDirty(true); - cmc.deactivate(); - VFSLeaf newPosterFile = posterFile; - + VFSLeaf posterFile = (VFSLeaf) ((FolderEvent) event).getItem(); if(source == posterUploadForm){ - videoManager.setPosterframeResizeUploadfile(videoResource, newPosterFile); + videoManager.setPosterframeResizeUploadfile(videoResource, posterFile); posterFile.delete(); } else { - videoManager.setPosterframe(videoResource, newPosterFile); + videoManager.setPosterframe(videoResource, posterFile); } updatePosterImage(ureq, videoResource); - // cleanup controllers - if (posterSelectionForm != null) { - removeAsListenerAndDispose(posterSelectionForm); - posterSelectionForm = null; - } - if (posterUploadForm != null) { - removeAsListenerAndDispose(posterUploadForm); - posterUploadForm = null; - } - if (cmc != null) { - removeAsListenerAndDispose(cmc); - cmc = null; - } - } + cmc.deactivate(); + cleanUp(); + } else if(cmc == source) { + cleanUp(); } } + + private void cleanUp() { + removeAsListenerAndDispose(posterSelectionForm); + removeAsListenerAndDispose(posterUploadForm); + removeAsListenerAndDispose(cmc); + posterSelectionForm = null; + posterUploadForm = null; + cmc = null; + } private void doReplaceVideo(UserRequest ureq){ posterSelectionForm = new VideoPosterSelectionForm(ureq, getWindowControl(), videoResource); listenTo(posterSelectionForm); - cmc = new CloseableModalController(getWindowControl(), "close", posterSelectionForm.getInitialComponent()); - listenTo(cmc); - cmc.activate(); + + if(posterSelectionForm.hasProposals()) { + String title = translate("video.config.poster.replace"); + cmc = new CloseableModalController(getWindowControl(), "close", posterSelectionForm.getInitialComponent(), true, title, true); + listenTo(cmc); + cmc.activate(); + } else { + showWarning("warning.no.poster.proposals"); + cleanUp(); + } } private void doUploadVideo(UserRequest ureq){ posterUploadForm = new VideoPosterUploadForm(ureq, getWindowControl(), videoResource); listenTo(posterUploadForm); - cmc = new CloseableModalController(getWindowControl(), "close", posterUploadForm.getInitialComponent()); + + String title = translate("video.config.poster.upload"); + cmc = new CloseableModalController(getWindowControl(), "close", posterUploadForm.getInitialComponent(), + true, title, true); listenTo(cmc); cmc.activate(); } private void updatePosterImage(UserRequest ureq, OLATResource video){ - posterFile = videoManager.getPosterframe(video); + VFSLeaf posterFile = videoManager.getPosterframe(video); VFSContainer masterContainer = posterFile.getParentContainer(); VideoMediaMapper mediaMapper = new VideoMediaMapper(masterContainer); String mediaUrl = registerMapper(ureq, mediaMapper); String serverUrl = Settings.createServerURI(); displayContainer.contextPut("serverUrl", serverUrl); displayContainer.contextPut("mediaUrl", mediaUrl); + displayContainer.setDirty(true); } } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java b/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java index ecccc5fd634915e77de64229282064a6546909c3..4c8634634622da938c1e4f5393fbd632f4346fa7 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java +++ b/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java @@ -21,14 +21,14 @@ package org.olat.modules.video.ui; import java.io.File; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; -import org.jcodec.common.FileChannelWrapper; -import org.jcodec.containers.mp4.demuxer.MP4Demuxer; import org.olat.core.commons.modules.bc.FolderEvent; +import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.link.Link; @@ -37,13 +37,16 @@ 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.helpers.Settings; -import org.olat.core.util.CodeHelper; +import org.olat.core.gui.media.ForbiddenMediaResource; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.media.NotFoundMediaResource; +import org.olat.core.util.WebappHelper; import org.olat.core.util.vfs.LocalFolderImpl; import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.VFSMediaResource; import org.olat.modules.video.VideoManager; -import org.olat.modules.video.manager.VideoMediaMapper; import org.olat.resource.OLATResource; import org.springframework.beans.factory.annotation.Autowired; @@ -57,76 +60,87 @@ public class VideoPosterSelectionForm extends BasicController { private static final String FILENAME_PREFIX_PROPOSAL_POSTER = "proposalPoster"; private VFSContainer tmpContainer; + @Autowired private VideoManager videoManager; - private VelocityContainer proposalLayout = createVelocityContainer("video_poster_proposal"); + private final VelocityContainer proposalLayout; - private Map<String, String> generatedPosters = new HashMap<String, String>(); + private static final int STEP = 24; + private final boolean hasProposals; public VideoPosterSelectionForm(UserRequest ureq, WindowControl wControl, OLATResource videoResource) { super(ureq, wControl); + proposalLayout = createVelocityContainer("video_poster_proposal"); // posters are generated in tmp. - File tmp = new File(System.getProperty("java.io.tmpdir"), CodeHelper.getGlobalForeverUniqueID()); - tmp.mkdirs(); - tmpContainer = new LocalFolderImpl(tmp); - - long duration = 1000; + tmpContainer = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), "poster_" + UUID.randomUUID())); - File videoFile = videoManager.getVideoFile(videoResource); - RandomAccessFile accessFile; - try { - accessFile = new RandomAccessFile(videoFile,"r"); - FileChannel ch = accessFile.getChannel(); - FileChannelWrapper in = new FileChannelWrapper(ch); - MP4Demuxer demuxer1 = new MP4Demuxer(in); - duration = demuxer1.getVideoTrack().getFrameCount(); - } catch (Exception | AssertionError e) { - logError("Error while accessing master video::" + videoFile.getAbsolutePath(), e); + List<String> proposals = generatePosterProposals(videoResource); + proposalLayout.contextPut("proposals", proposals); + hasProposals = !proposals.isEmpty(); + + if(!proposals.isEmpty()) { + String mediaUrl = registerMapper(ureq, new PosterMapper()); + proposalLayout.contextPut("mediaUrl", mediaUrl); } + putInitialPanel(proposalLayout); + } + + public boolean hasProposals() { + return hasProposals; + } + + private List<String> generatePosterProposals(OLATResource videoResource) { + long frames = videoManager.getVideoFrameCount(videoResource); - long firstThirdDuration = duration/7; - for (int currentFrame = 0; currentFrame <= duration; currentFrame += firstThirdDuration) { + long framesStepping = frames / 7; + if(framesStepping == 0) { + framesStepping = 256; + } + long maxAdjust = framesStepping / STEP; + + int proposalCounter = 0; + List<String> generatedPosters = new ArrayList<>(); + + a_a: + for (int currentFrame = 0; currentFrame <= frames && generatedPosters.size() < 8; currentFrame += framesStepping) { try { - String fileName; - boolean imgBlack; int adjust = 0; - do { - fileName = FILENAME_PREFIX_PROPOSAL_POSTER + (currentFrame+adjust) + FILENAME_POSTFIX_JPG; - VFSLeaf posterProposal = tmpContainer.createChildLeaf(fileName); - imgBlack = videoManager.getFrameWithFilter(videoResource, (currentFrame+adjust), duration, posterProposal); - int step = 24; - if (currentFrame + step <= duration) { - adjust += step; + String fileName = FILENAME_PREFIX_PROPOSAL_POSTER + (proposalCounter++) + FILENAME_POSTFIX_JPG; + VFSLeaf posterProposal = tmpContainer.createChildLeaf(fileName); + + boolean imgBlack = true; + for(int i=0; i<maxAdjust && imgBlack; i++) { + imgBlack = videoManager.getFrameWithFilter(videoResource, (currentFrame+adjust), frames, posterProposal); + + if (currentFrame + STEP <= frames) { + adjust += STEP; } else { - adjust -= step; + adjust -= STEP; } // set lower bound to avoid endless loop - if (currentFrame+adjust < 0) { + if (currentFrame + adjust < 0) { // if all poster images are mostly black just take current frame - videoManager.getFrame(videoResource, currentFrame, posterProposal); - break; + if(videoManager.getFrame(videoResource, currentFrame, posterProposal)) { + break; + } else { + break a_a; + } } - } while (imgBlack); - VideoMediaMapper mediaMapper = new VideoMediaMapper(tmpContainer); - String mediaUrl = registerMapper(ureq, mediaMapper); - String serverUrl = Settings.createServerURI(); - proposalLayout.contextPut("serverUrl", serverUrl); + } - Link button = LinkFactory.createButton(String.valueOf(currentFrame), proposalLayout, this); + Link button = LinkFactory.createButton(fileName, proposalLayout, this); button.setCustomEnabledLinkCSS("o_video_poster_selct"); button.setCustomDisplayText(translate("poster.select")); button.setUserObject(fileName); - - generatedPosters.put(mediaUrl + "/" + fileName, String.valueOf(currentFrame)); + generatedPosters.add(fileName); } catch (Exception | AssertionError e) { - logError("Error while creating poster images for video::" + videoFile.getAbsolutePath(), e); + logError("Error while creating poster images for video: " + videoResource, e); } } - proposalLayout.contextPut("pics", generatedPosters); - - putInitialPanel(proposalLayout); + + return generatedPosters; } @Override @@ -142,10 +156,24 @@ public class VideoPosterSelectionForm extends BasicController { protected void event(UserRequest ureq, Component source, Event event) { if (source instanceof Link) { Link button = (Link) source; - VFSLeaf posterFile = (VFSLeaf)tmpContainer.resolve((String)button.getUserObject()); - if (posterFile != null) { + VFSItem posterFile = tmpContainer.resolve((String)button.getUserObject()); + if (posterFile instanceof VFSLeaf) { fireEvent(ureq, new FolderEvent(FolderEvent.UPLOAD_EVENT, posterFile)); } } } + + private class PosterMapper implements Mapper { + @Override + public MediaResource handle(String relPath, HttpServletRequest request) { + if(relPath != null && relPath.contains("..") && !relPath.endsWith(FILENAME_POSTFIX_JPG)) { + return new ForbiddenMediaResource(relPath); + } + VFSItem mediaFile = tmpContainer.resolve(relPath); + if (mediaFile instanceof VFSLeaf){ + return new VFSMediaResource((VFSLeaf)mediaFile); + } + return new NotFoundMediaResource(relPath); + } + } } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/ui/VideoPosterUploadForm.java b/src/main/java/org/olat/modules/video/ui/VideoPosterUploadForm.java index d8fb3c3c0f9ae6563de4f0a5ce24c66956b5ca6f..55a835871d91bf4e93409b4b0dc16c5da92caf42 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoPosterUploadForm.java +++ b/src/main/java/org/olat/modules/video/ui/VideoPosterUploadForm.java @@ -30,6 +30,7 @@ import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.util.vfs.LocalFolderImpl; import org.olat.core.util.vfs.Quota; @@ -43,7 +44,6 @@ import org.olat.resource.OLATResource; * @author dfurrer, dirk.furrer@frentix.com, http://www.frentix.com * */ - public class VideoPosterUploadForm extends FormBasicController { private OLATResource videoResource; private long remainingSpace; @@ -82,26 +82,32 @@ public class VideoPosterUploadForm extends FormBasicController { FormLayoutContainer buttonGroupLayout = FormLayoutContainer.createButtonLayout("buttonGroupLayout", getTranslator()); formLayout.add(buttonGroupLayout); buttonGroupLayout.setElementCssClass("o_sel_upload_buttons"); + uifactory.addFormCancelButton("cancel", buttonGroupLayout, ureq, getWindowControl()); uifactory.addFormSubmitButton("track.upload", buttonGroupLayout); } @Override protected void formOK(UserRequest ureq) { - if ( posterField.isUploadSuccess()) { + if (posterField.isUploadSuccess()) { if (remainingSpace != -1) { if (posterField.getUploadFile().length() / 1024 > remainingSpace) { posterField.setErrorKey("QuotaExceeded", null); posterField.getUploadFile().delete(); return; } - }else{ + } else { fireEvent(ureq, new FolderEvent(FolderEvent.UPLOAD_EVENT, posterField.moveUploadFileTo(metaDataFolder))); } } } + @Override + protected void formCancelled(UserRequest ureq) { + fireEvent(ureq, Event.CANCELLED_EVENT); + } + @Override protected void doDispose() { - // TODO Auto-generated method stub + // } } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/ui/_content/video_poster_proposal.html b/src/main/java/org/olat/modules/video/ui/_content/video_poster_proposal.html index 429005a276de000a3960bde46d8a39c69fa1f193..a560b1f13076ebd10f98cf63119fbac3632a5039 100644 --- a/src/main/java/org/olat/modules/video/ui/_content/video_poster_proposal.html +++ b/src/main/java/org/olat/modules/video/ui/_content/video_poster_proposal.html @@ -1,8 +1,8 @@ <div class="o_video_poster_select"> <h2>$r.translate("poster.select")</h2> - #foreach( $poster in $pics.entrySet()) - <div class="o_video_poster" style="background-image: url('$poster.key')"> - $r.render($poster.value) + #foreach( $poster in $proposals) + <div class="o_video_poster" style="background-image: url('${mediaUrl}/$poster')"> + $r.render($poster) </div> #end </div> diff --git a/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_de.properties index 8079baf957e77c9cd73797684d7a0253a74fa6c5..aa20d917b4bfe89b2ec0b9713c2fe24d2acb2fa3 100644 --- a/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_de.properties @@ -121,3 +121,4 @@ video.replace.desc=Bitte w\u00E4hlen Sie eine Video-Datei aus. Um die alte Datei video.replace.upload=Video Datei Upload video.replaced=Ihre Video-Datei wurde ersetzt und weitere Aufl\u00F6sungen werden transkodiert. resource.error=Konnte Resource nicht \u00F6ffnen. +warning.no.poster.proposals=Posterbilder konnte nicht generiert werden. diff --git a/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_en.properties index 9f28e08b33f7db9bba551cd0f916b9033ed9f90c..e222d4e1c8bac870129a87180969befe1f334745 100644 --- a/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/video/ui/_i18n/LocalStrings_en.properties @@ -121,3 +121,5 @@ video.replace.desc=Please choose a Video-File from your File-System. To replace video.replace.upload=Video File Upload video.replaced=Your Video file has been replaced, and new Resolutions are beeing transcoded. resource.error=Could not open resource. +warning.no.poster.proposals=The proposals for poster frame could be generated. +