/**
* <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.manager;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.math.RoundingMode;
import java.nio.channels.FileChannel;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.imageio.ImageIO;
import org.apache.commons.io.FilenameUtils;
import org.jcodec.api.FrameGrab;
import org.jcodec.common.FileChannelWrapper;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.services.image.Crop;
import org.olat.core.commons.services.image.ImageService;
import org.olat.core.commons.services.image.Size;
import org.olat.core.commons.services.video.MovieService;
import org.olat.core.gui.translator.Translator;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.Formatter;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.vfs.LocalFileImpl;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.VFSConstants;
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.VFSManager;
import org.olat.core.util.vfs.VFSStatus;
import org.olat.core.util.vfs.filters.VFSItemSuffixFilter;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.fileresource.FileResourceManager;
import org.olat.fileresource.types.ResourceEvaluation;
import org.olat.modules.video.VideoManager;
import org.olat.modules.video.VideoMeta;
import org.olat.modules.video.VideoMetadata;
import org.olat.modules.video.VideoModule;
import org.olat.modules.video.VideoTranscoding;
import org.olat.modules.video.model.TranscodingCount;
import org.olat.modules.video.model.VideoMetaImpl;
import org.olat.modules.video.model.VideoMetadataImpl;
import org.olat.modules.video.ui.VideoChapterTableRow;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryEntryImportExport;
import org.olat.repository.RepositoryEntryImportExport.RepositoryEntryImport;
import org.olat.repository.RepositoryManager;
import org.olat.resource.OLATResource;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Manager for Videoressource
*
* @author dfurrer, dirk.furrer@frentix.com, http://www.frentix.com
*
*/
@Service("videoManager")
public class VideoManagerImpl implements VideoManager {
private static final String CR = System.lineSeparator();
private static final String ENCODING = "utf-8";
protected static final String DIRNAME_REPOENTRY = "repoentry";
public static final String FILETYPE_MP4 = "mp4";
private static final String FILETYPE_JPG = "jpg";
private static final String FILETYPE_SRT = "srt";
private static final String FILENAME_POSTER_JPG = "poster.jpg";
private static final String FILENAME_VIDEO_MP4 = "video.mp4";
private static final String FILENAME_CHAPTERS_VTT = "chapters.vtt";
private static final String FILENAME_VIDEO_METADATA_XML = "video_metadata.xml";
private static final String DIRNAME_MASTER = "master";
public static final String TRACK = "track_";
public static final String DOT = "." ;
private static final SimpleDateFormat displayDateFormat = new SimpleDateFormat("HH:mm:ss");
private static final SimpleDateFormat vttDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
@Autowired
private MovieService movieService;
@Autowired
private VideoModule videoModule;
@Autowired
private RepositoryManager repositoryManager;
@Autowired
private VideoTranscodingDAO videoTranscodingDao;
@Autowired
private VideoMetadataDAO videoMetadataDao;
@Autowired
private Scheduler scheduler;
@Autowired
private ImageService imageHelper;
private static final OLog log = Tracing.createLoggerFor(VideoManagerImpl.class);
/**
* get the configured posterframe
*/
@Override
public VFSLeaf getPosterframe(OLATResource videoResource) {
VFSLeaf posterFrame = resolveFromMasterContainer(videoResource, FILENAME_POSTER_JPG);
return posterFrame;
}
/**
* set a specific VFSLeaf as posterframe in video metadata
*/
@Override
public void setPosterframe(OLATResource videoResource, VFSLeaf posterframe){
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf newPoster = VFSManager.resolveOrCreateLeafFromPath(masterContainer, FILENAME_POSTER_JPG);
VFSManager.copyContent(posterframe, newPoster);
// Update also repository entry image, use new posterframe
VFSLeaf posterImage = (VFSLeaf)masterContainer.resolve(FILENAME_POSTER_JPG);
if (posterImage != null) {
RepositoryEntry repoEntry = repositoryManager.lookupRepositoryEntry(videoResource, true);
repositoryManager.setImage(posterImage, repoEntry);
}
}
/**
* Sets the posterframe resize uploadfile. Tries to fit image to dimensions of video.
*
* @param videoResource the video resource
* @param posterframe the newPosterFile
*/
public void setPosterframeResizeUploadfile(OLATResource videoResource, VFSLeaf newPosterFile) {
VideoMeta videoMetadata = getVideoMetadata(videoResource);
Size posterRes = imageHelper.getSize(newPosterFile, FILETYPE_JPG);
// file size needs to be bigger than target resolution, otherwise use image as it comes
if (posterRes != null
&& posterRes.getHeight() != 0
&& posterRes.getWidth() != 0
&& posterRes.getHeight() >= videoMetadata.getHeight()
&& posterRes.getWidth() >= videoMetadata.getWidth()) {
VFSLeaf oldPosterFile = getPosterframe(videoResource);
oldPosterFile.delete();
VFSContainer masterContainer = getMasterContainer(videoResource);
LocalFileImpl newPoster = (LocalFileImpl) masterContainer.createChildLeaf(FILENAME_POSTER_JPG);
// to shrink image file, resolution ratio needs to be equal, otherwise crop from top left corner
if (posterRes.getHeight() / posterRes.getWidth() == videoMetadata.getHeight() / videoMetadata.getWidth()) {
imageHelper.scaleImage(newPosterFile, newPoster, videoMetadata.getWidth(), videoMetadata.getHeight(), true);
} else {
Crop cropSelection = new Crop(0, 0, videoMetadata.getHeight(), videoMetadata.getWidth());
imageHelper.cropImage(((LocalFileImpl) newPosterFile).getBasefile(), newPoster.getBasefile(), cropSelection);
}
} else {
setPosterframe(videoResource, newPosterFile);
}
}
// /**
// * add a subtitle-track to the videoresource
// */
// @Override
// public void addTrack(OLATResource videoResource, String lang, VFSLeaf trackFile){
// VideoMetadata metaData = readVideoMetadataFile(videoResource);
// metaData.addTrack(lang, trackFile.getName());
// writeVideoMetadataFile(metaData, videoResource);
// }
/**
* get a specific subtitle-track of the videoresource
*/
@Override
public VFSLeaf getTrack(OLATResource videoResource, String lang) {
String path = TRACK + lang + DOT + FILETYPE_SRT;
return resolveFromMasterContainer(videoResource, path);
}
/**
* remove a specific track from the videoresource
*/
@Override
public void removeTrack(OLATResource videoResource, String lang){
VFSContainer vfsContainer = getMasterContainer(videoResource);
for (VFSItem item : vfsContainer.getItems(new VFSItemSuffixFilter(new String[]{FILETYPE_SRT}))) {
if (item.getName().contains(lang)) {
item.delete();
}
}
}
/**
* get all tracks saved in the video metadata as map
*/
@Override
public Map<String, VFSLeaf> getAllTracks(OLATResource videoResource) {
Map<String, VFSLeaf> tracks = new HashMap<>();
VFSContainer vfsContainer = getMasterContainer(videoResource);
for (VFSItem item : vfsContainer.getItems(new VFSItemSuffixFilter(new String[]{FILETYPE_SRT}))) {
String itemname = item.getName();
String key = itemname.substring(itemname.indexOf("_") + 1, itemname.indexOf("."));
tracks.put(key, resolveFromMasterContainer(videoResource, itemname));
}
// VideoMetadata metaData = readVideoMetadataFile(videoResource);
// for(Entry<String, String> trackEntry : metaData.getAllTracks().entrySet()){
// for(Entry<String, String> trackEntry : alltracks.entrySet()){
// tracks.put(trackEntry.getKey(), resolveFromMasterContainer(videoResource, trackEntry.getValue()));
// }
return tracks;
}
/**
* return the chapter file as a VFSLeaf
*/
@Override
public boolean hasChapters(OLATResource videoResource){
VFSContainer vfsContainer = getMasterContainer(videoResource);
VFSLeaf webvtt = (VFSLeaf) vfsContainer.resolve(FILENAME_CHAPTERS_VTT);
return (webvtt != null && webvtt.getSize() > 0);
}
/**
* write the the given frame at frameNumber in the frame leaf
* @param videoResource videoresource
* @param frameNumber the frameNumber at which the frame should be taken from
* @param frame the VFSLeaf to write the picked image to
*/
@Override
public boolean getFrame(OLATResource videoResource, int frameNumber, VFSLeaf frame) {
File videoFile = ((LocalFileImpl)getMasterVideoFile(videoResource)).getBasefile();
try (RandomAccessFile randomAccessFile = new RandomAccessFile(videoFile, "r")) {
FileChannel ch = randomAccessFile.getChannel();
FileChannelWrapper in = new FileChannelWrapper(ch);
FrameGrab frameGrab = new FrameGrab(in).seekToFrameSloppy(frameNumber);
OutputStream frameOutputStream = frame.getOutputStream(false);
BufferedImage bufImg = frameGrab.getFrame();
ImageIO.write(bufImg, "JPG", frameOutputStream);
// close everything to prevent resource leaks
frameOutputStream.close();
in.close();
ch.close();
return true;
} catch (Exception | AssertionError e) {
log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e);
return false;
}
}
@Override
public boolean getFrameWithFilter(OLATResource videoResource, int frameNumber, long duration, VFSLeaf frame) {
File videoFile = ((LocalFileImpl) getMasterVideoFile(videoResource)).getBasefile();
BufferedImage bufImg = null;
boolean imgBlack = true;
int countBlack = 0;
try (RandomAccessFile randomAccessFile = new RandomAccessFile(videoFile, "r")) {
OutputStream frameOutputStream = frame.getOutputStream(false);
FileChannel ch = randomAccessFile.getChannel();
FileChannelWrapper in = new FileChannelWrapper(ch);
FrameGrab frameGrab = new FrameGrab(in).seekToFrameSloppy(frameNumber);
bufImg = frameGrab.getFrame();
int xmin = bufImg.getMinX();
int ymin = bufImg.getMinY();
int xmax = xmin + bufImg.getWidth();
int ymax = ymin + bufImg.getHeight();
int pixelCount = bufImg.getWidth() * bufImg.getHeight();
for (int x = xmin; x < xmax; x++) {
for (int y = ymin; y < ymax; y++) {
int rgb = bufImg.getRGB(x, y);
// int alpha = (0xff000000 & rgb) >>> 24;
int r = (0x00ff0000 & rgb) >> 16;
int g = (0x0000ff00 & rgb) >> 8;
int b = (0x000000ff & rgb);
if (r < 30 && g < 30 && b < 30) {
countBlack++;
}
}
}
if (countBlack > (int) (0.7F * pixelCount)) {
imgBlack = true;
} else {
imgBlack = false;
ImageIO.write(bufImg, "JPG", frameOutputStream);
}
// avoid endless loop
if (frameNumber > duration) {
imgBlack = false;
}
// close everything to prevent resource leaks
frameOutputStream.close();
in.close();
ch.close();
return imgBlack;
} catch (Exception | AssertionError e) {
log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e);
return false;
}
}
/**
* get the File of the videoresource
*/
@Override
public File getVideoFile(OLATResource videoResource) {
VFSContainer masterContainer = getMasterContainer(videoResource);
LocalFileImpl videoFile = (LocalFileImpl) masterContainer.resolve(FILENAME_VIDEO_MP4);
return videoFile.getBasefile();
}
/**
* Resolve the given path to a file in the master directory and return it
*
* @param videoResource
* corresponding videoresource
* @param path
* path to the videofile
* @return VFSLeaf of videofile of resource
*/
private VFSLeaf resolveFromMasterContainer(OLATResource videoResource, String path){
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSItem item = masterContainer.resolve(path);
if(item instanceof VFSLeaf){
return (VFSLeaf) item;
}else{
return null;
}
}
// /**
// * Write the metdatadata-xml in the videoresource folder
// * @param metaData
// * @param videoResource
// */
// private void writeVideoMetadataFile(VideoMetadata metaData, OLATResource videoResource){
// VFSContainer baseContainer= FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
// VFSLeaf metaDataFile = VFSManager.resolveOrCreateLeafFromPath(baseContainer, FILENAME_VIDEO_METADATA_XML);
// XStreamHelper.writeObject(XStreamHelper.createXStreamInstance(), metaDataFile, metaData);
// }
@Override
public boolean isMetadataFileValid(OLATResource videoResource) {
VFSContainer baseContainer = FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
VFSLeaf metaDataFile = (VFSLeaf) baseContainer.resolve(FILENAME_VIDEO_METADATA_XML);
try {
VideoMetadata meta = (VideoMetadata) XStreamHelper.readObject(XStreamHelper.createXStreamInstance(), metaDataFile);
return meta != null;
} catch (Exception e) {
log.error("Error while parsing XStream file for videoResource::" + videoResource, e);
return false;
}
}
@Override
public VideoMetadata readVideoMetadataFile(OLATResource videoResource){
VFSContainer baseContainer= FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
VFSLeaf metaDataFile = (VFSLeaf) baseContainer.resolve(FILENAME_VIDEO_METADATA_XML);
try {
return (VideoMetadata) XStreamHelper.readObject(XStreamHelper.createXStreamInstance(), metaDataFile);
} catch (Exception 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);
meta.setHeight(600);
return meta;
}
}
@Override
public void startTranscodingProcessIfEnabled(OLATResource video) {
if (videoModule.isTranscodingEnabled()) {
startTranscodingProcess(video);
}
}
@Override
public VideoTranscoding retranscodeFailedVideoTranscoding(VideoTranscoding videoTranscoding) {
return videoTranscodingDao.updateTranscodingStatus(videoTranscoding);
}
@Override
public void startTranscodingProcess(OLATResource video) {
List<VideoTranscoding> existingTranscodings = getVideoTranscodings(video);
VideoMeta videoMetadata = getVideoMetadata(video);
int height = videoMetadata.getHeight();
// 1) setup transcoding job for original file size
createTranscodingIfNotCreatedAlready(video, height, VideoTranscoding.FORMAT_MP4, existingTranscodings);
// 2) setup transcoding jobs for all configured sizes below the original size
int[] resolutions = videoModule.getTranscodingResolutions();
for (int resolution : resolutions) {
if (height <= resolution) {
continue;
}
createTranscodingIfNotCreatedAlready(video, resolution, VideoTranscoding.FORMAT_MP4, existingTranscodings);
}
// 3) Start transcoding immediately, force job execution
if (videoModule.isTranscodingLocal()) {
try {
JobDetail detail = scheduler.getJobDetail("videoTranscodingJobDetail", Scheduler.DEFAULT_GROUP);
scheduler.triggerJob(detail.getName(), detail.getGroup());
} catch (SchedulerException e) {
log.error("Error while starting video transcoding job", e);
}
}
}
/**
* Helper to check if a transcoding already exists and only create if not
* @param video
* @param resolution
* @param format
* @param existingTranscodings
*/
private void createTranscodingIfNotCreatedAlready(OLATResource video, int resolution, String format, List<VideoTranscoding> existingTranscodings) {
boolean found = false;
for (VideoTranscoding videoTranscoding : existingTranscodings) {
if (videoTranscoding.getResolution() == resolution) {
found = true;
break;
}
}
if (!found) {
videoTranscodingDao.createVideoTranscoding(video, resolution, format);
}
}
@Override
public List<VideoTranscoding> getVideoTranscodings(OLATResource video){
List<VideoTranscoding> videoTranscodings = videoTranscodingDao.getVideoTranscodings(video);
return videoTranscodings;
}
@Override
public List<VideoTranscoding> getAllVideoTranscodings() {
List<VideoTranscoding> videoTranscodings = videoTranscodingDao.getAllVideoTranscodings();
return videoTranscodings;
}
@Override
public List<TranscodingCount> getAllVideoTranscodingsCount() {
List<TranscodingCount> allVideoTranscodings = videoTranscodingDao.getAllVideoTranscodingsCount();
return allVideoTranscodings;
}
@Override
public List<TranscodingCount> getAllVideoTranscodingsCountSuccess(int errorcode) {
List<TranscodingCount> allVideoTranscodings = videoTranscodingDao.getAllVideoTranscodingsCountSuccess(errorcode);
return allVideoTranscodings;
}
@Override
public List<TranscodingCount> getAllVideoTranscodingsCountFails(int errorcode) {
List<TranscodingCount> allVideoTranscodings = videoTranscodingDao.getAllVideoTranscodingsCountFails(errorcode);
return allVideoTranscodings;
}
@Override
public List<VideoTranscoding> getOneVideoResolution(int resolution) {
List<VideoTranscoding> oneResolution = videoTranscodingDao.getOneVideoResolution(resolution);
return oneResolution;
}
@Override
public String getAspectRatio(int width, int height) {
DecimalFormat df = new DecimalFormat("#.##");
df.setRoundingMode(RoundingMode.FLOOR);
String ratioCalculated = df.format(width / (height + 1.0));
String ratioString = "unknown";
switch (ratioCalculated) {
case "1.2":
ratioString = "6:5 Fox Movietone";
break;
case "1.25":
ratioString = "5:4 TV";
break;
case "1.33":
ratioString = "4:3 TV";
break;
case "1.37":
ratioString = "11:8 Academy standard film";
break;
case "1.41":
ratioString = "A4";
break;
case "1.43":
ratioString = "IMAX";
break;
case "1.5":
ratioString = "3:2 35mm";
break;
case "1.6":
ratioString = "16:10 Computer";
break;
case "1.61":
ratioString = "16.18:10 The golden ratio";
break;
case "1.66":
ratioString = "5:3 Super 16mm";
break;
case "1.77":
ratioString = "16:9 HD video";
break;
case "1.78":
ratioString = "16:9 HD video";
break;
case "1.85":
ratioString = "1.85:1 Widescreen cinema";
break;
case "2.35":
ratioString = "2.35:1 Widescreen cinema";
break;
case "2.39":
ratioString = "2.39:1 Widescreen cinema";
break;
case "2.41":
ratioString = "2.414:1 The silver ratio";
break;
default :
ratioString = width + ":" + height;
}
return ratioString;
}
@Override
public String getDisplayTitleForResolution(int resolution, Translator translator) {
int[] resolutions = videoModule.getTranscodingResolutions();
boolean knownResolution = IntStream.of(resolutions).anyMatch(x -> x == resolution);
String title = (knownResolution ? translator.translate("quality.resolution." + resolution) : resolution + "p");
return title;
}
@Override
public boolean hasMasterContainer (OLATResource videoResource) {
VFSContainer baseContainer = FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
VFSContainer masterContainer = (VFSContainer) baseContainer.resolve(DIRNAME_MASTER);
return masterContainer != null & masterContainer.exists();
}
@Override
public VFSContainer getMasterContainer(OLATResource videoResource) {
VFSContainer baseContainer = FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
VFSContainer masterContainer = VFSManager.resolveOrCreateContainerFromPath(baseContainer, DIRNAME_MASTER);
return masterContainer;
}
@Override
public VFSContainer getTranscodingContainer(OLATResource videoResource) {
VFSContainer baseContainer = videoModule.getTranscodingBaseContainer();
VFSContainer resourceTranscodingContainer = VFSManager.getOrCreateContainer(baseContainer,
String.valueOf(videoResource.getResourceableId()));
return resourceTranscodingContainer;
}
@Override
public VFSLeaf getMasterVideoFile(OLATResource videoResource) {
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf videoFile = (VFSLeaf) masterContainer.resolve(FILENAME_VIDEO_MP4);
return videoFile;
}
@Override
public VideoExportMediaResource getVideoExportMediaResource(RepositoryEntry repoEntry) {
OLATResource videoResource = repoEntry.getOlatResource();
OlatRootFolderImpl baseContainer= FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
// 1) dump repo entry metadata to resource folder
LocalFolderImpl repoentryContainer = (LocalFolderImpl)VFSManager.resolveOrCreateContainerFromPath(baseContainer, DIRNAME_REPOENTRY);
RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(repoEntry, repoentryContainer.getBasefile());
importExport.exportDoExportProperties();
// 2) package everything in resource folder to streaming zip resource
VideoExportMediaResource exportResource = new VideoExportMediaResource(baseContainer, repoEntry.getDisplayname());
return exportResource;
}
@Override
public void validateVideoExportArchive(File file, ResourceEvaluation eval) {
ZipFile zipFile;
try {
zipFile = new ZipFile(file);
// 1) Check if it contains a metadata file
// ZipEntry metadataEntry = zipFile.getEntry(VideoManagerImpl.FILENAME_VIDEO_METADATA_XML);
// VideoMetadata videoMetadataImpl = null;
// if (metadataEntry != null) {// does no harm
// InputStream metaDataStream = zipFile.getInputStream(metadataEntry);
// videoMetadataImpl = (VideoMetadata) XStreamHelper.readObject(XStreamHelper.createXStreamInstance(), metaDataStream);
// if (videoMetadataImpl != null) {
// eval.setValid(true);
// }
// }
// 2) Propose title from repo metadata
ZipEntry repoMetadataEntry = zipFile.getEntry(DIRNAME_REPOENTRY + "/" + RepositoryEntryImportExport.PROPERTIES_FILE);
RepositoryEntryImport repoMetadata = null;
if (repoMetadataEntry != null) {
InputStream repoMetaDataStream = zipFile.getInputStream(repoMetadataEntry);
repoMetadata = RepositoryEntryImportExport.getConfiguration(repoMetaDataStream);
if (repoMetadata != null) {
eval.setDisplayname(repoMetadata.getDisplayname());
}
}
zipFile.close();
} catch (Exception e) {
log.error("Error while checking for video resource archive", e);
}
}
@Override
public boolean importFromMasterFile(RepositoryEntry repoEntry, VFSLeaf masterVideo) {
OLATResource videoResource = repoEntry.getOlatResource();
// 1) copy master video to final destination with standard name
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf targetFile = VFSManager.resolveOrCreateLeafFromPath(masterContainer, FILENAME_VIDEO_MP4);
VFSManager.copyContent(masterVideo, targetFile);
masterVideo.delete();
// calculate video duration
long duration = movieService.getDuration(targetFile, FILETYPE_MP4);
if (duration != -1) {
repoEntry.setExpenditureOfWork(Formatter.formatTimecode(duration));
}
// generate a poster image, use 20th frame as a default
VFSLeaf posterResource = VFSManager.resolveOrCreateLeafFromPath(masterContainer, FILENAME_POSTER_JPG);
getFrame(videoResource, 20, posterResource);
// 2) Set poster image for repo entry
VFSLeaf posterImage = (VFSLeaf)masterContainer.resolve(FILENAME_POSTER_JPG);
if (posterImage != null) {
repositoryManager.setImage(posterImage, repoEntry);
}
return true;
}
@Override
public Size getVideoResolutionFromOLATResource (OLATResource videoResource) {
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf targetFile = (VFSLeaf) masterContainer.resolve(FILENAME_VIDEO_MP4);
Size videoSize = movieService.getSize(targetFile, FILETYPE_MP4);
if (videoSize == null) {
videoSize = new Size(800, 600, false);
}
return videoSize;
}
@Override
public void exchangePoster (OLATResource videoResource) {
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf posterResource = VFSManager.resolveOrCreateLeafFromPath(masterContainer, FILENAME_POSTER_JPG);
getFrame(videoResource, 20, posterResource);
// Update also repository entry image, use new posterframe
VFSLeaf posterImage = (VFSLeaf)masterContainer.resolve(FILENAME_POSTER_JPG);
if (posterImage != null) {
RepositoryEntry repoEntry = repositoryManager.lookupRepositoryEntry(videoResource, true);
repositoryManager.setImage(posterImage, repoEntry);
}
}
@Override
public void updateVideoMetadata (OLATResource videoResource,VFSLeaf uploadVideo) {
VideoMeta meta = getVideoMetadata(videoResource);
Size dimensions = movieService.getSize(uploadVideo, VideoManagerImpl.FILETYPE_MP4);
// update video duration
long duration = movieService.getDuration(uploadVideo, VideoTranscoding.FORMAT_MP4);
if (duration != -1) {
String length = Formatter.formatTimecode(duration);
meta.setSize(uploadVideo.getSize());
meta.setWidth(dimensions.getWidth());
meta.setHeight(dimensions.getHeight());
meta.setFormat(FilenameUtils.getExtension(uploadVideo.getName()));
meta.setLength(length);
}
}
@Override
public boolean importFromExportArchive(RepositoryEntry repoEntry, VFSLeaf exportArchive) {
OLATResource videoResource = repoEntry.getOlatResource();
// 1) unzip archive
VFSContainer baseContainer= FileResourceManager.getInstance().getFileResourceRootImpl(videoResource);
ZipUtil.unzip(exportArchive, baseContainer);
exportArchive.delete();
// 2) update metadata from the repo entry export
LocalFolderImpl repoentryContainer = (LocalFolderImpl) baseContainer.resolve(DIRNAME_REPOENTRY);
if (repoentryContainer != null) {
RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(repoentryContainer.getBasefile());
importExport.setRepoEntryPropertiesFromImport(repoEntry);
// now delete the import folder, not used anymore
repoentryContainer.delete();
}
// 3) Set poster image for repo entry
VFSContainer masterContainer = getMasterContainer(videoResource);
VFSLeaf posterImage = (VFSLeaf)masterContainer.resolve(FILENAME_POSTER_JPG);
if (posterImage != null) {
repositoryManager.setImage(posterImage, repoEntry);
}
return true;
}
@Override
public VideoTranscoding updateVideoTranscoding(VideoTranscoding videoTranscoding) {
return videoTranscodingDao.updateTranscoding(videoTranscoding);
}
@Override
public void copyVideo(OLATResource sourceResource, OLATResource targetResource) {
// 1) Copy files on disk
File sourceFileroot = FileResourceManager.getInstance().getFileResourceRootImpl(sourceResource).getBasefile();
File targetFileroot = FileResourceManager.getInstance().getFileResourceRootImpl(targetResource).getBasefile();
FileUtils.copyDirContentsToDir(sourceFileroot, targetFileroot, false, "copyVideoResource");
// 2) Trigger transcoding in background
if (videoModule.isTranscodingEnabled()) {
startTranscodingProcess(targetResource);
}
}
@Override
public boolean deleteVideoTranscodings(OLATResource videoResource) {
videoTranscodingDao.deleteVideoTranscodings(videoResource);
VFSStatus deleteStatus = getTranscodingContainer(videoResource).delete();
return (deleteStatus == VFSConstants.YES ? true : false);
}
@Override
public boolean deleteVideoMetadata(OLATResource videoResource) {
int deleted = videoMetadataDao.deleteVideoMetadata(videoResource);
return 0 < deleted;
}
@Override
public List<VideoTranscoding> getVideoTranscodingsPendingAndInProgress() {
return videoTranscodingDao.getVideoTranscodingsPendingAndInProgress();
}
@Override
public List<VideoTranscoding> getFailedVideoTranscodings() {
return videoTranscodingDao.getFailedVideoTranscodings();
}
@Override
public void deleteVideoTranscoding(VideoTranscoding videoTranscoding) {
videoTranscodingDao.deleteVideoTranscoding(videoTranscoding);
VFSContainer container = getTranscodingContainer(videoTranscoding.getVideoResource());
VFSLeaf videoFile = (VFSLeaf) container.resolve(videoTranscoding.getResolution() + FILENAME_VIDEO_MP4);
if( videoFile != null ) {
videoFile.delete();
}
}
@Override
public List<Integer> getMissingTranscodings(OLATResource videoResource){
//get resolutions which are turned on in the videomodule
int[] configuredResolutions = videoModule.getTranscodingResolutions();
//turn the int[]-Array into a List
List<Integer> configResList = IntStream.of(configuredResolutions).boxed().collect(Collectors.toList());
List<VideoTranscoding> videoTranscodings = getVideoTranscodings(videoResource);
for(VideoTranscoding videoTranscoding:videoTranscodings){
Integer resolution = videoTranscoding.getResolution();
configResList.remove(resolution);
}
return configResList;
}
@Override
public VideoTranscoding createTranscoding(OLATResource video, int resolution,String format) {
return videoTranscodingDao.createVideoTranscoding(video, resolution, format);
}
@Override
public void saveChapters (List<VideoChapterTableRow> chapters, OLATResource videoResource){
displayDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
vttDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
VFSContainer vfsContainer = getMasterContainer(videoResource);
VFSLeaf webvtt = (VFSLeaf) vfsContainer.resolve(FILENAME_CHAPTERS_VTT);
if (webvtt == null) {
webvtt = vfsContainer.createChildLeaf(FILENAME_CHAPTERS_VTT);
}
if (chapters.size() == 0){
webvtt.delete();
return;
}
StringBuilder vttString = new StringBuilder("WEBVTT").append(CR);
for (int i = 0; i < chapters.size(); i++) {
vttString.append(CR).append("Chapter "+ (i+1)).append(CR);
vttString.append(vttDateFormat.format(chapters.get(i).getBegin()));
vttString.append(" --> ");
vttString.append(vttDateFormat.format(chapters.get(i).getEnd())).append(CR);
vttString.append(chapters.get(i).getChapterName().replaceAll(CR, " "));
vttString.append(CR);
}
final BufferedOutputStream bos = new BufferedOutputStream(webvtt.getOutputStream(false));
FileUtils.save(bos, vttString.toString(), ENCODING);
try {
bos.close();
} catch (IOException e) {
log.error("chapter.vtt could not be saved for videoResource::" + videoResource, e);
}
}
/**
* reads an existing webvtt file to provide for display and to further process.
*
* @param List<VideoChapterTableRow> chapters the chapters
* @param OLATResource videoResource the video resource
*/
public void loadChapters(List<VideoChapterTableRow> chapters, OLATResource videoResource) {
chapters.clear();
displayDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
vttDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
VFSContainer vfsContainer = getMasterContainer(videoResource);
VFSLeaf webvtt = (VFSLeaf) vfsContainer.resolve(FILENAME_CHAPTERS_VTT);
if (webvtt != null && webvtt.exists()) {
try {
BufferedReader webvttReader = new BufferedReader(new InputStreamReader(webvtt.getInputStream()));
String thisLine, regex = " --> ";
while ((thisLine = webvttReader.readLine()) != null) {
if (thisLine.contains(regex)) {
String[] interval = thisLine.split(regex);
Date begin = vttDateFormat.parse(interval[0]);
Date end = vttDateFormat.parse(interval[1]);
StringBuilder chapterTitle = new StringBuilder();
String title;
while ((title = webvttReader.readLine()) != null) {
if (title.isEmpty() || title.contains(regex))
break;
chapterTitle.append(title).append(CR);
}
chapters.add(new VideoChapterTableRow(chapterTitle.toString().replaceAll(CR, " "),
displayDateFormat.format(begin), begin, end));
}
}
webvttReader.close();
} catch (Exception e) {
log.error("Unable to load WEBVTT File for resource::" + videoResource,e);
}
}
}
@Override
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;
}
@Override
public List<VideoMetaImpl> getAllVideoResourcesMetadata() {
List<VideoMetaImpl> metadata = videoMetadataDao.getAllVideoResourcesMetadata();
return metadata;
}
@Override
public boolean hasVideoMetadata(OLATResource videoResource) {
return videoMetadataDao.getVideoMetadata(videoResource) != null;
}
@Override
public VideoMetaImpl getVideoMetadata(OLATResource videoResource) {
VideoMetaImpl meta = videoMetadataDao.getVideoMetadata(videoResource);
if (meta == null) {
return new VideoMetaImpl(800, 600, 5000);
}
return meta;
}
@Override
public VideoMeta createVideoMetadata(RepositoryEntry repoEntry, long size, String fileName) {
return videoMetadataDao.createVideoMetadata(repoEntry, size, fileName);
}
@Override
public List<RepositoryEntry> getAllVideoRepoEntries(String typename) {
return videoMetadataDao.getAllVideoRepoEntries(typename);
}
@Override
public boolean hasVideoFile(OLATResource videoResource) {
VFSContainer masterContainer = getMasterContainer(videoResource);
LocalFileImpl videoFile = (LocalFileImpl) masterContainer.resolve(FILENAME_VIDEO_MP4);
return videoFile != null && videoFile.exists();
}
}