/** * <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 java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.List; import org.jcodec.api.FrameGrab; import org.jcodec.common.FileChannelWrapper; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.demuxer.MP4Demuxer; import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.image.spi.ImageHelperImpl; import org.olat.core.commons.services.thumbnail.CannotGenerateThumbnailException; import org.olat.core.commons.services.thumbnail.FinalSize; import org.olat.core.commons.services.thumbnail.ThumbnailSPI; import org.olat.core.commons.services.video.spi.FLVParser; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; import org.olat.core.util.WorkThreadInformations; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.core.util.vfs.VFSLeaf; import org.olat.ims.cp.ui.VFSCPNamedItem; import org.springframework.stereotype.Service; /** * * Initial date: 04.04.2014<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ @Service("movieService") public class MovieServiceImpl implements MovieService, ThumbnailSPI { private static final OLog log = Tracing.createLoggerFor(MovieServiceImpl.class); private static final List<String> extensions = new ArrayList<>(); private static final List<String> fourCCs = new ArrayList<>(); static { // supported file extensions extensions.add("mp4"); extensions.add("m4v"); extensions.add("mov"); // supported fourCC for H264 codec fourCCs.add("avc1"); fourCCs.add("davc"); fourCCs.add("h264"); fourCCs.add("x264"); fourCCs.add("vssh"); } @Override public List<String> getExtensions() { return extensions; } @Override public Size getSize(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 null; } 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); org.jcodec.common.model.Size size = demuxer1.getMovie().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; h = 0 - h; } if (w == 0) { // Case 3: portrait movie from iOS: movie dimensions are not set, but there // something in the track box. try { // This code is the way it is just because I don't know // how to safely read the rotation/portrait/landscape // flag of the movie. Those mp4 guys are really // secretive folks, did not find any documentation about // this. Best guess. org.jcodec.common.model.Size size2 = demuxer1.getVideoTrack().getBox().getCodedSize(); w = size2.getHeight(); h = size2.getWidth(); } catch(Exception e) { log.debug("can not get size from box " + e.getMessage()); } } return new Size(w, h, false); } catch (Exception | AssertionError e) { log.error("Cannot extract size of: " + media, e); } } else if(suffix.equals("flv")) { try(InputStream stream = new FileInputStream(file)) { FLVParser infos = new FLVParser(); infos.parse(stream); if(infos.getWidth() > 0 && infos.getHeight() > 0) { int w = infos.getWidth(); int h = infos.getHeight(); return new Size(w, h, false); } } catch (Exception e) { log.error("Cannot extract size of: " + media, e); } } return null; } @Override public long getDuration(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); MovieBox movie = demuxer1.getMovie(); long duration = movie.getDuration(); int timescale = movie.getTimescale(); if (timescale < 1) { timescale = 1; } // Simple calculation. Ignore NTSC and other issues for now return duration / timescale * 1000; } catch (Exception | AssertionError e) { log.error("Cannot extract duration of: " + media, e); } } return -1; } @Override public boolean isMP4(VFSLeaf media, String fileName) { File file = null; if(media instanceof VFSCPNamedItem) { media = ((VFSCPNamedItem)media).getDelegate(); } if(media instanceof LocalFileImpl) { file = ((LocalFileImpl)media).getBasefile(); } if(file == null) { return false; } String suffix = FileUtils.getFileSuffix(fileName); 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); String fourCC = demuxer1.getVideoTrack().getFourcc(); if (fourCCs.contains(fourCC.toLowerCase())) { return true; } log.info("Movie file::" + fileName + " has correct suffix::" + suffix + " but fourCC::" + fourCC + " not in our list of supported codecs."); } catch (Exception | Error e) { // anticipated exception, is not an mp4 file } } return false; } @Override public FinalSize generateThumbnail(VFSLeaf file, VFSLeaf thumbnailFile, int maxWidth, int maxHeight, boolean fill) throws CannotGenerateThumbnailException { FinalSize size = null; if(file instanceof LocalFileImpl && thumbnailFile instanceof LocalFileImpl) { try { WorkThreadInformations.setInfoFiles(null, file); WorkThreadInformations.set("Generate thumbnail (video) VFSLeaf=" + file); File baseFile = ((LocalFileImpl)file).getBasefile(); File scaledImage = ((LocalFileImpl)thumbnailFile).getBasefile(); BufferedImage frame = FrameGrab.getFrame(baseFile, 20); Size scaledSize = ImageHelperImpl.calcScaledSize(frame, maxWidth, maxHeight); if(ImageHelperImpl.writeTo(frame, scaledImage, scaledSize, "jpeg")) { size = new FinalSize(scaledSize.getWidth(), scaledSize.getHeight()); } //NullPointerException can be thrown if the jcodec cannot handle the codec of the movie //ArrayIndexOutOfBoundsException } catch (Exception | AssertionError e) { log.error("", e); } finally { WorkThreadInformations.unset(); } } return size; } }