diff --git a/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java b/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..46029699c5ca3988e598cc5ca149d07231896977 --- /dev/null +++ b/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java @@ -0,0 +1,63 @@ +/** + * <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.core.commons.services.video; + +import java.awt.image.BufferedImage; + +import org.apache.logging.log4j.Logger; +import org.jcodec.api.transcode.PixelStore; +import org.jcodec.api.transcode.PixelStore.LoanerPicture; +import org.jcodec.api.transcode.PixelStoreImpl; +import org.jcodec.api.transcode.filters.ScaleFilter; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.olat.core.commons.services.image.Size; +import org.olat.core.logging.Tracing; + +/** + * + * Initial date: 7 mars 2021<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class JCodecHelper { + + private static final Logger log = Tracing.createLoggerFor(JCodecHelper.class); + + private JCodecHelper() { + // + } + + public static BufferedImage scale(Size movieSize, Picture picture, BufferedImage bufImg) { + if(movieSize != null && picture != null && bufImg != null + && (bufImg.getWidth() != movieSize.getWidth() || bufImg.getHeight() != movieSize.getHeight())) { + try { + ScaleFilter filter = new ScaleFilter(movieSize.getWidth(), movieSize.getHeight()); + PixelStore store = new PixelStoreImpl(); + LoanerPicture lPicture = filter.filter(picture, store); + bufImg = AWTUtil.toBufferedImage(lPicture.getPicture()); + } catch (Exception e) { + log.error("", e); + } + } + return bufImg; + } + +} 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 6d85c13da70a3f62b8064bb009b02a131b3821da..54f31594aa5b9d51d0a92fce394a2743791f7415 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 @@ -33,6 +33,7 @@ import org.jcodec.api.FrameGrab; import org.jcodec.common.Codec; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.io.FileChannelWrapper; +import org.jcodec.common.model.Picture; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.demuxer.MP4Demuxer; import org.jcodec.scale.AWTUtil; @@ -102,11 +103,12 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { FileChannelWrapper in = new FileChannelWrapper(ch); MP4Demuxer demuxer1 = MP4Demuxer.createMP4Demuxer(in)) { - - org.jcodec.common.model.Size size = demuxer1.getMovie().getDisplaySize(); + MovieBox movieBox = demuxer1.getMovie(); + org.jcodec.common.model.Size size = movieBox.getDisplaySize(); // Case 1: standard case, get dimension from movie int w = size.getWidth(); int h = size.getHeight(); + // Case 2: landscape movie from iOS: width and height is negative, no dunny why if (w < 0 && h < 0) { w = 0 - w; @@ -131,7 +133,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { } return new Size(w, h, false); } catch (Exception | AssertionError e) { - log.error("Cannot extract size of: " + media, e); + log.error("Cannot extract size of: {}", media, e); } } else if(suffix.equals("flv")) { try(InputStream stream = new FileInputStream(file)) { @@ -143,7 +145,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { return new Size(w, h, false); } } catch (Exception e) { - log.error("Cannot extract size of: " + media, e); + log.error("Cannot extract size of: {}", media, e); } } @@ -178,7 +180,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { // Simple calculation. Ignore NTSC and other issues for now return duration / timescale * 1000l; } catch (Exception | AssertionError e) { - log.error("Cannot extract duration of: " + media, e); + log.error("Cannot extract duration of: {}", media, e); } } @@ -205,7 +207,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { MP4Demuxer demuxer1 = MP4Demuxer.createMP4Demuxer(in)) { return demuxer1.getVideoTrack().getMeta().getTotalFrames(); } catch (Exception | AssertionError e) { - log.error("Cannot extract num. of frames of: " + media, e); + log.error("Cannot extract num. of frames of: {}", media, e); } } @@ -252,9 +254,12 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { WorkThreadInformations.setInfoFiles(null, file); WorkThreadInformations.set("Generate thumbnail (video) VFSLeaf=" + file); - File baseFile = ((LocalFileImpl)file).getBasefile(); + File movieFile = ((LocalFileImpl)file).getBasefile(); + Size movieSize = getSize(file, "mp4"); File scaledImage = ((LocalFileImpl)thumbnailFile).getBasefile(); - BufferedImage frame = AWTUtil.toBufferedImage(FrameGrab.getFrameFromFile(baseFile, 20)); + Picture picture = FrameGrab.getFrameFromFile(movieFile, 20); + BufferedImage frame = AWTUtil.toBufferedImage(picture); + frame = JCodecHelper.scale(movieSize, picture, frame); Size scaledSize = ImageHelperImpl.calcScaledSize(frame, maxWidth, maxHeight); if(ImageHelperImpl.writeTo(frame, scaledImage, scaledSize, "jpeg")) { size = new FinalSize(scaledSize.getWidth(), scaledSize.getHeight()); @@ -269,4 +274,6 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { } return size; } + + } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/VideoManager.java b/src/main/java/org/olat/modules/video/VideoManager.java index fb721458d0a82082f5cb245b1c2c54324f9bf92a..236855fd15831c9ec3c3e3ab0e03d6e45d669cda 100644 --- a/src/main/java/org/olat/modules/video/VideoManager.java +++ b/src/main/java/org/olat/modules/video/VideoManager.java @@ -522,6 +522,6 @@ public interface VideoManager { * @param frame resource * @return true if image proposal is mostly black */ - public boolean getFrameWithFilter(VFSLeaf video, int frameNumber, long duration, VFSLeaf frame); + public boolean getFrameWithFilter(VFSLeaf video, Size movieSize, int frameNumber, long duration, VFSLeaf frame); } \ No newline at end of file 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 e840fbb31ad66e0a820cee515779fbc0e7eb83ee..8fda24a363f2ce3ad2f237a35ad1d0103efd747d 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java +++ b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java @@ -51,6 +51,7 @@ import org.apache.logging.log4j.Logger; import org.jcodec.api.FrameGrab; import org.jcodec.common.io.FileChannelWrapper; import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.model.Picture; import org.jcodec.scale.AWTUtil; import org.olat.core.commons.persistence.DB; import org.olat.core.commons.services.image.Crop; @@ -58,6 +59,7 @@ import org.olat.core.commons.services.image.ImageService; import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.vfs.VFSMetadata; import org.olat.core.commons.services.vfs.VFSRepositoryService; +import org.olat.core.commons.services.video.JCodecHelper; import org.olat.core.commons.services.video.MovieService; import org.olat.core.gui.translator.Translator; import org.olat.core.id.Identity; @@ -289,11 +291,15 @@ public class VideoManagerImpl implements VideoManager { public boolean getFrame(VFSLeaf video, int frameNumber, VFSLeaf frame) { File videoFile = ((LocalFileImpl)video).getBasefile(); + Size movieSize = movieService.getSize(video, FILETYPE_MP4); + try (FileChannelWrapper in = NIOUtils.readableChannel(videoFile)) { FrameGrab frameGrab = FrameGrab.createFrameGrab(in).seekToFrameSloppy(frameNumber); OutputStream frameOutputStream = frame.getOutputStream(false); - BufferedImage bufImg = AWTUtil.toBufferedImage(frameGrab.getNativeFrame()); + Picture picture = frameGrab.getNativeFrame(); + BufferedImage bufImg = AWTUtil.toBufferedImage(picture); + bufImg = JCodecHelper.scale(movieSize, picture, bufImg); ImageIO.write(bufImg, "JPG", frameOutputStream); // close everything to prevent resource leaks @@ -301,22 +307,24 @@ public class VideoManagerImpl implements VideoManager { return true; } catch (Exception | AssertionError e) { - log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e); + log.error("Could not get frame::{} for video::{}", frameNumber, videoFile.getAbsolutePath(), e); return false; } } @Override - public boolean getFrameWithFilter(VFSLeaf video, int frameNumber, long duration, VFSLeaf frame) { + public boolean getFrameWithFilter(VFSLeaf video, Size movieSize, int frameNumber, long duration, VFSLeaf frame) { File videoFile = ((LocalFileImpl)video).getBasefile(); BufferedImage bufImg = null; boolean imgBlack = true; int countBlack = 0; + try (FileChannelWrapper in = NIOUtils.readableChannel(videoFile)) { OutputStream frameOutputStream = frame.getOutputStream(false); FrameGrab frameGrab = FrameGrab.createFrameGrab(in).seekToFrameSloppy(frameNumber); - bufImg = AWTUtil.toBufferedImage(frameGrab.getNativeFrame()); + Picture picture = frameGrab.getNativeFrame(); + bufImg = AWTUtil.toBufferedImage(picture); int xmin = bufImg.getMinX(); int ymin = bufImg.getMinY(); @@ -340,6 +348,7 @@ public class VideoManagerImpl implements VideoManager { imgBlack = true; } else { imgBlack = false; + bufImg = JCodecHelper.scale(movieSize, picture, bufImg); ImageIO.write(bufImg, "JPG", frameOutputStream); } // avoid endless loop @@ -351,7 +360,7 @@ public class VideoManagerImpl implements VideoManager { return imgBlack; } catch (Exception | AssertionError e) { - log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e); + log.error("Could not get frame: {} for video: {}", frameNumber, videoFile.getAbsolutePath(), e); return false; } } @@ -404,7 +413,7 @@ public class VideoManagerImpl implements VideoManager { try { return (VideoMetadata) XStreamHelper.readObject(XStreamHelper.createXStreamInstance(), metaDataFile); } catch (Exception e) { - log.error("Error while parsing XStream file for videoResource::" + videoResource, e); + log.error("Error while parsing XStream file for videoResource::{}", videoResource, e); // return an empty, so at least it displays something and not an error VideoMetadata meta = new VideoMetadataImpl(); meta.setWidth(800); @@ -534,7 +543,7 @@ public class VideoManagerImpl implements VideoManager { DecimalFormat df = new DecimalFormat("#.##"); df.setRoundingMode(RoundingMode.FLOOR); String ratioCalculated = df.format(width / (height + 1.0)); - String ratioString = "unknown"; + String ratioString; switch (ratioCalculated) { case "1.2": @@ -1079,7 +1088,7 @@ public class VideoManagerImpl implements VideoManager { try(OutputStream bos = new BufferedOutputStream(webvtt.getOutputStream(false))) { FileUtils.save(bos, vttString.toString(), ENCODING); } catch (IOException e) { - log.error("chapter.vtt could not be saved for videoResource::" + videoResource, e); + log.error("chapter.vtt could not be saved for videoResource::{}", videoResource, e); } } diff --git a/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java b/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java index 0e7e18b7c9578ce97418e41b7dee9ce13b8e2b4f..ad18fd5d47fcc3711967752a6abeea3ff7033a95 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java +++ b/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java @@ -107,14 +107,14 @@ public class VideoTranscodingJob extends JobWithDB { for (VideoTranscoding videoTrans : videoTranscodings) { String transcoder = videoTrans.getTranscoder(); if (transcoder == null) { - log.info("Start transcoding video with resolution::" + videoTrans.getResolution() - + " for video resource::" + videoTrans.getVideoResource().getResourceableId()); + log.info("Start transcoding video with resolution: {} for video resource: {}", + videoTrans.getResolution(), videoTrans.getVideoResource().getResourceableId()); videoTrans.setTranscoder(VideoTranscoding.TRANSCODER_LOCAL); videoTranscoding = videoManager.updateVideoTranscoding(videoTrans); break; } else if (transcoder.equals(VideoTranscoding.TRANSCODER_LOCAL)) { - log.info("Continue with transcoding video with resolution::" + videoTrans.getResolution() - + " for video resource::" + videoTrans.getVideoResource().getResourceableId()); + log.info("Continue with transcoding video with resolution: {} for video resource: {}", + videoTrans.getResolution(), videoTrans.getVideoResource().getResourceableId()); videoTranscoding = videoTrans; break; } @@ -238,7 +238,7 @@ public class VideoTranscodingJob extends JobWithDB { if(exitCode == 0) { videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_DONE); } else { - log.error("Exit code " + videoTranscoding + ":" + exitCode); + log.error("Exit code {}:{}", videoTranscoding, exitCode); videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_ERROR); } videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); @@ -269,7 +269,7 @@ public class VideoTranscodingJob extends JobWithDB { int end = line.indexOf("."); if (end != -1 && end < 5) { String percent = line.substring(2, end); - log.debug("Output: " + percent); + log.debug("Output: {}", percent); // update version file for UI try { videoTranscoding.setStatus(Integer.parseInt(percent)); @@ -303,7 +303,7 @@ public class VideoTranscodingJob extends JobWithDB { String line = null; while ((line = berr.readLine()) != null) { - log.debug("Error: " + line); + log.debug("Error: {}", line); } } catch (IOException e) { log.error("", e); 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 33c37a40f01ec19204816343c0bea752097ac5e7..f0547e0d496d9bfe5bf854d2789daedeb5b85f6e 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java +++ b/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java @@ -28,6 +28,8 @@ import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.olat.core.commons.modules.bc.FolderEvent; +import org.olat.core.commons.services.image.Size; +import org.olat.core.commons.services.video.MovieService; import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -47,6 +49,7 @@ 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.VideoFormat; import org.olat.modules.video.VideoManager; import org.olat.modules.video.VideoMeta; import org.olat.resource.OLATResource; @@ -64,12 +67,16 @@ public class VideoPosterSelectionForm extends BasicController { private VFSLeaf tmpFile; private VFSContainer tmpContainer; - @Autowired - private VideoManager videoManager; private final VelocityContainer proposalLayout; + private Size movieSize; private static final int STEP = 24; private final boolean hasProposals; + + @Autowired + private MovieService movieService; + @Autowired + private VideoManager videoManager; public VideoPosterSelectionForm(UserRequest ureq, WindowControl wControl, OLATResource videoResource, VideoMeta videoMetadata) { @@ -83,8 +90,14 @@ public class VideoPosterSelectionForm extends BasicController { if(StringHelper.containsNonWhitespace(videoMetadata.getUrl())) { videoFile = videoManager.downloadTmpVideo(videoResource, videoMetadata); tmpFile = videoFile;// delete temporary file + if(videoMetadata.getVideoFormat() == VideoFormat.m3u8) { + movieSize = movieService.getSize(videoFile, VideoFormat.mp4.name()); + } } else { videoFile = videoManager.getMasterVideoFile(videoResource); + if(videoMetadata.getVideoFormat() == VideoFormat.mp4) { + movieSize = movieService.getSize(videoFile, VideoFormat.mp4.name()); + } } List<String> proposals = generatePosterProposals(videoFile); @@ -116,7 +129,7 @@ public class VideoPosterSelectionForm extends BasicController { private List<String> generatePosterProposals(VFSLeaf videoFile) { long frames = videoManager.getVideoFrameCount(videoFile); - + long framesStepping = frames / 7; if(framesStepping == 0) { framesStepping = 256; @@ -135,7 +148,7 @@ public class VideoPosterSelectionForm extends BasicController { boolean imgBlack = true; for(int i=0; i<maxAdjust && imgBlack; i++) { - imgBlack = videoManager.getFrameWithFilter(videoFile, (currentFrame+adjust), frames, posterProposal); + imgBlack = videoManager.getFrameWithFilter(videoFile, movieSize, (currentFrame+adjust), frames, posterProposal); if (currentFrame + STEP <= frames) { adjust += STEP;