Skip to content
Snippets Groups Projects
Commit 5242f459 authored by srosse's avatar srosse
Browse files

OO-5339: handle poster of anamorphic videos

parent b0c3a386
No related branches found
No related tags found
No related merge requests found
/**
* <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;
}
}
......@@ -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
......@@ -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
......@@ -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);
}
}
......
......@@ -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);
......
......@@ -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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment