diff --git a/src/main/java/org/olat/modules/video/VideoManager.java b/src/main/java/org/olat/modules/video/VideoManager.java index 48ce846a43a654aefc3567423ceaf4f9af3ee88f..722889a182cddb7ef2a1c8ee8aca55b9a0c70f74 100644 --- a/src/main/java/org/olat/modules/video/VideoManager.java +++ b/src/main/java/org/olat/modules/video/VideoManager.java @@ -72,6 +72,13 @@ public interface VideoManager { */ public abstract void startTranscodingProcess(OLATResource video); + /** + * Get all video transcodings for a specific video resource, sorted by + * resolution, highes resolution first + * + * @param video + * @return + */ public abstract List<VideoTranscoding> getVideoTranscodings(OLATResource video); /** @@ -177,4 +184,9 @@ public interface VideoManager { */ public abstract boolean deleteVideoTranscodings(OLATResource videoResource); + /** + * @return List of video transcodings which have not yet been done + */ + public abstract List<VideoTranscoding> getVideoTranscodingsPendingAndInProgress(); + } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/VideoModule.java b/src/main/java/org/olat/modules/video/VideoModule.java index e7a86b2bad400ba5555d44bb1b527be9017cd41a..709fbcc0ccac964a823f464324fa8c96b0c1f4d9 100644 --- a/src/main/java/org/olat/modules/video/VideoModule.java +++ b/src/main/java/org/olat/modules/video/VideoModule.java @@ -48,6 +48,8 @@ public class VideoModule extends AbstractSpringModule { private static final String VIDEO_ENABLED = "video.enabled"; private static final String VIDEOCOURSENODE_ENABLED = "video.coursenode.enabled"; private static final String VIDEOTRANSCODING_ENABLED = "video.transcoding.enabled"; + private static final String VIDEOTRANSCODING_LOCAL = "video.transcoding.local"; + @Value("${video.enabled:true}") private boolean enabled; @@ -56,6 +58,8 @@ public class VideoModule extends AbstractSpringModule { // transcoding related configuration @Value("${video.transcoding.enabled:false}") private boolean transcodingEnabled; + @Value("${video.transcoding.local:true}") + private boolean transcodingLocal; @Value("${video.transcoding.resolutions}") private String transcodingResolutions; @Value("${video.transcoding.taskset.cpuconfig}") @@ -65,6 +69,7 @@ public class VideoModule extends AbstractSpringModule { private int[] transcodingResolutionsArr = new int[] { 1080,720,480,360 }; + @Autowired public VideoModule(CoordinatorManager coordinatorManager) { super(coordinatorManager); @@ -104,11 +109,17 @@ public class VideoModule extends AbstractSpringModule { transcodingEnabled = "true".equals(enabledTranscodingObj); } + String localTranscodingObj = getStringPropertyValue(VIDEOTRANSCODING_LOCAL, true); + if(StringHelper.containsNonWhitespace(localTranscodingObj)) { + transcodingLocal = "true".equals(localTranscodingObj); + } + log.info("video.enabled=" + isEnabled()); log.info("video.coursenode.enabled=" + isCoursenodeEnabled()); log.info("video.transcoding.enabled=" + isTranscodingEnabled()); log.info("video.transcoding.resolutions=" + Arrays.toString(getTranscodingResolutions())); log.info("video.transcoding.taskset.cpuconfig=" + getTranscodingTasksetConfig()); + log.info("video.transcoding.local=" + isTranscodingLocal()); } /** @@ -120,6 +131,7 @@ public class VideoModule extends AbstractSpringModule { */ public int[] getTranscodingResolutions() { return transcodingResolutionsArr; + //TODO: implement GUI for reading/Setting } /** @@ -192,4 +204,15 @@ public class VideoModule extends AbstractSpringModule { setStringProperty(VIDEOTRANSCODING_ENABLED, Boolean.toString(transcodingEnabled), true); //TODO: check all video resources if there are missing versions } + + public boolean isTranscodingLocal() { + return isTranscodingEnabled() && transcodingLocal; + //TODO: implement GUI for reading/Setting + } + + public void setTranscoding(boolean transcodingLocal) { + this.transcodingLocal = transcodingLocal; + setStringProperty(VIDEOTRANSCODING_LOCAL, Boolean.toString(transcodingLocal), true); + } + } 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 0e159fd4c9c184feacb56eab515afc69ed55b7b7..61decc02d22093e860613ab8c2f42c542516cb51 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java +++ b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java @@ -28,7 +28,6 @@ import java.io.RandomAccessFile; import java.math.RoundingMode; import java.nio.channels.FileChannel; import java.text.DecimalFormat; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; @@ -42,7 +41,6 @@ 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.Size; -import org.olat.core.commons.services.taskexecutor.TaskExecutorManager; import org.olat.core.commons.services.video.MovieService; import org.olat.core.gui.translator.Translator; import org.olat.core.logging.OLog; @@ -70,6 +68,9 @@ 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; @@ -95,9 +96,9 @@ public class VideoManagerImpl implements VideoManager { @Autowired private RepositoryManager repositoryManager; @Autowired - private TaskExecutorManager taskManager; - @Autowired private VideoTranscodingDAO videoTranscodingDao; + @Autowired + private Scheduler scheduler; private static final OLog log = Tracing.createLoggerFor(VideoManagerImpl.class); @@ -250,34 +251,57 @@ public class VideoManagerImpl implements VideoManager { @Override public void startTranscodingProcess(OLATResource video) { - //TODO: check for existing version, add option to force rebuild of all versions - //TODO: GUI to admin console to manage transcoding resolutions + List<VideoTranscoding> existingTranscodings = getVideoTranscodings(video); VideoMetadata videoMetadata = readVideoMetadataFile(video); int height = videoMetadata.getHeight(); // 1) setup transcoding job for original file size - VideoTranscoding videoTranscoding = videoTranscodingDao.createVideoTranscoding(video, height, VideoTranscoding.FORMAT_MP4); - VideoTranscodingTask task = new VideoTranscodingTask(video, videoTranscoding); - taskManager.execute(task, null, video, null, new Date()); + 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; } - videoTranscoding = videoTranscodingDao.createVideoTranscoding(video, resolution, VideoTranscoding.FORMAT_MP4); - task = new VideoTranscodingTask(video, videoTranscoding); - taskManager.execute(task, null, video, null, new Date()); + 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); + } } - // start transcoding immediately - taskManager.executeTaskToDo(); } + /** + * 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 String getAspectRatio(int width, int height) { @@ -433,8 +457,13 @@ public class VideoManagerImpl implements VideoManager { VideoMetadata metaData = new VideoMetadataImpl(); // calculate video size Size videoSize = movieService.getSize(targetFile, FILETYPE_MP4); - metaData.setWidth(videoSize.getWidth()); - metaData.setHeight(videoSize.getHeight()); + if (videoSize != null) { + metaData.setWidth(videoSize.getWidth()); + metaData.setHeight(videoSize.getHeight()); + } else { + metaData.setWidth(600); + metaData.setHeight(800); + } // generate a poster image, use 20th frame as a default VFSLeaf posterResource = VFSManager.resolveOrCreateLeafFromPath(masterContainer, FILENAME_POSTER_JPG); getFrame(videoResource, 20, posterResource); @@ -510,5 +539,10 @@ public class VideoManagerImpl implements VideoManager { return (deleteStatus == VFSConstants.YES ? true : false); } + @Override + public List<VideoTranscoding> getVideoTranscodingsPendingAndInProgress() { + return videoTranscodingDao.getVideoTranscodingsPendingAndInProgress(); + } + } diff --git a/src/main/java/org/olat/modules/video/manager/VideoTranscodingDAO.java b/src/main/java/org/olat/modules/video/manager/VideoTranscodingDAO.java index 69fc46a550a0f26d84958b6a08d6bedac540c337..20c407a310e441354f8a62c4dab78d591d3d45c9 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoTranscodingDAO.java +++ b/src/main/java/org/olat/modules/video/manager/VideoTranscodingDAO.java @@ -33,6 +33,7 @@ import org.springframework.stereotype.Service; * DAO implementation for manipulating VideoTranscoding objects * * Initial date: 05.05.2016<br> + * * @author gnaegi, gnaegi@frentix.com, http://www.frentix.com * */ @@ -43,13 +44,15 @@ public class VideoTranscodingDAO { private DB dbInstance; /** - * Factory method to create and persist new video transcoding objects for a given video resource + * Factory method to create and persist new video transcoding objects for a + * given video resource + * * @param videoResource * @param resolution * @param format * @return */ - public VideoTranscoding createVideoTranscoding(OLATResource videoResource, int resolution, String format) { + VideoTranscoding createVideoTranscoding(OLATResource videoResource, int resolution, String format) { VideoTranscodingImpl videoTranscoding = new VideoTranscodingImpl(); videoTranscoding.setCreationDate(new Date()); videoTranscoding.setLastModified(videoTranscoding.getCreationDate()); @@ -60,30 +63,70 @@ public class VideoTranscodingDAO { dbInstance.getCurrentEntityManager().persist(videoTranscoding); return videoTranscoding; } - - public List<VideoTranscoding> getVideoTranscodings(OLATResource videoResource) { - StringBuilder sb = new StringBuilder(); - sb.append("select trans from videotranscoding as trans") - .append(" inner join fetch trans.videoResource as res") - .append(" where res.key=:resourceKey"); - return dbInstance.getCurrentEntityManager() - .createQuery(sb.toString(), VideoTranscoding.class) - .setParameter("resourceKey", videoResource.getKey()) - .getResultList(); - } - public VideoTranscoding updateTranscoding(VideoTranscoding videoTranscoding) { - ((VideoTranscodingImpl)videoTranscoding).setLastModified(new Date()); + /** + * Merge updated video transcoding, persist on DB + * + * @param videoTranscoding + * @return Updated transcoding object + */ + VideoTranscoding updateTranscoding(VideoTranscoding videoTranscoding) { + ((VideoTranscodingImpl) videoTranscoding).setLastModified(new Date()); VideoTranscoding trans = dbInstance.getCurrentEntityManager().merge(videoTranscoding); return trans; } - public int deleteVideoTranscodings(OLATResource videoResource) { + /** + * Delete all video transcoding objects for a given video resource + * + * @param videoResource + * @return + */ + int deleteVideoTranscodings(OLATResource videoResource) { String deleteQuery = "delete from videotranscoding where fk_resource_id=:resourceKey"; - return dbInstance.getCurrentEntityManager() - .createQuery(deleteQuery).setParameter("resourceKey", videoResource.getKey()) - .executeUpdate(); + return dbInstance.getCurrentEntityManager().createQuery(deleteQuery) + .setParameter("resourceKey", videoResource.getKey()).executeUpdate(); + } + + /** + * Delete a specifig video transcoding version + * + * @param videoTranscoding + */ + void deleteVideoTranscoding(VideoTranscoding videoTranscoding) { + dbInstance.getCurrentEntityManager().remove(videoTranscoding); + } + + /** + * Get all video transcodings for a specific video resource, sorted by + * resolution, highes resolution first + * + * @param videoResource + * @return + */ + List<VideoTranscoding> getVideoTranscodings(OLATResource videoResource) { + StringBuilder sb = new StringBuilder(); + sb.append("select trans from videotranscoding as trans") + .append(" inner join fetch trans.videoResource as res") + .append(" where res.key=:resourceKey") + .append(" order by trans.resolution desc"); + return dbInstance.getCurrentEntityManager().createQuery(sb.toString(), VideoTranscoding.class) + .setParameter("resourceKey", videoResource.getKey()).getResultList(); + } + + /** + * Get all video transcodings which are waiting for transcoding or are + * currently in transcoding in FIFO ordering + * + * @return + */ + List<VideoTranscoding> getVideoTranscodingsPendingAndInProgress() { + StringBuilder sb = new StringBuilder(); + sb.append("select trans from videotranscoding as trans") + .append(" inner join fetch trans.videoResource as res") + .append(" where trans.status != 100") + .append(" order by trans.creationDate asc, trans.id asc"); + return dbInstance.getCurrentEntityManager().createQuery(sb.toString(), VideoTranscoding.class).getResultList(); } - } 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 a24b9628adf94c160f07b1b71252042894da83e5..baea8285cd38cba634ca3a399a8c9ded1fcb1312 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java +++ b/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java @@ -19,9 +19,30 @@ */ package org.olat.modules.video.manager; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.olat.core.CoreSpringFactory; +import org.olat.core.commons.persistence.DBFactory; +import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.scheduler.JobWithDB; +import org.olat.core.commons.services.video.MovieService; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.vfs.LocalFileImpl; +import org.olat.core.util.vfs.LocalFolderImpl; +import org.olat.modules.video.VideoManager; +import org.olat.modules.video.VideoModule; +import org.olat.modules.video.VideoTranscoding; +import org.olat.resource.OLATResource; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; +import org.quartz.StatefulJob; /** * @@ -29,15 +50,200 @@ import org.quartz.JobExecutionException; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class VideoTranscodingJob extends JobWithDB { +public class VideoTranscodingJob extends JobWithDB implements StatefulJob { + private static final OLog log = Tracing.createLoggerFor(VideoTranscodingJob.class); /** * * @see org.olat.core.commons.services.scheduler.JobWithDB#executeWithDB(org.quartz.JobExecutionContext) */ @Override - public void executeWithDB(JobExecutionContext arg0) throws JobExecutionException { - System.out.println("Work"); + public void executeWithDB(JobExecutionContext context) throws JobExecutionException { + // uses StatefulJob interface to prevent concurrent job execution + doExecute(context); + } + + /** + * Implementation of job execution + * @param context + * @return + * @throws JobExecutionException + */ + private boolean doExecute(JobExecutionContext context) throws JobExecutionException { + VideoModule videoModule = CoreSpringFactory.getImpl(VideoModule.class); + if (!videoModule.isTranscodingLocal()) { + log.debug("Skipping execution of video transcoding job, local transcoding disabled"); + return false; + } + + // Find first one to work with + VideoManager videoManager = CoreSpringFactory.getImpl(VideoManager.class); + List<VideoTranscoding> videoTranscodings = videoManager.getVideoTranscodingsPendingAndInProgress(); + VideoTranscoding videoTranscoding = null; + 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()); + 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()); + videoTranscoding = videoTrans; + break; + } + } + + if (videoTranscoding == null) { + log.debug("Skipping execution of video transcoding job, no pending video transcoding found in database"); + return false; + } + // Ready transcode, forke process now + boolean success = forkTranscodingProcess(videoTranscoding); + + // Transcoding done, call execution again until no more videos to be + // processed. If an error happend, don't continue to not get into a loop + if (success) { + success = doExecute(context); + } + return success; + } + + /** + * Internal helper to fork a process with handbrake and read the values from the process + * @param videoTranscoding + * @return true: all ok; false: an error happend along the way + */ + private boolean forkTranscodingProcess(VideoTranscoding videoTranscoding) { + OLATResource video = videoTranscoding.getVideoResource(); + VideoModule videoModule = CoreSpringFactory.getImpl(VideoModule.class); + VideoManager videoManager = CoreSpringFactory.getImpl(VideoManager.class); + File masterFile = videoManager.getVideoFile(video); + File transcodingFolder = ((LocalFolderImpl)videoManager.getTranscodingContainer(video)).getBasefile(); + File transcodedFile = new File(transcodingFolder, Integer.toString(videoTranscoding.getResolution()) + masterFile.getName()); + // mark this as beeing transcoded by this local transcoder + videoTranscoding.setTranscoder(VideoTranscoding.TRANSCODER_LOCAL); + videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); + + ArrayList<String> cmd = new ArrayList<>(); + String tasksetConfig = videoModule.getTranscodingTasksetConfig(); + if (tasksetConfig != null && !"Mac OS X".equals(System.getProperty("os.name"))) { + cmd.add("taskset"); + cmd.add("-c"); + cmd.add(tasksetConfig); + } + cmd.add("HandBrakeCLI"); + cmd.add("-i"); + cmd.add(masterFile.getAbsolutePath()); + cmd.add("-o"); + cmd.add(transcodedFile.getAbsolutePath()); + cmd.add("--optimize"); + cmd.add("--preset"); + cmd.add("Normal"); + cmd.add("--height"); + cmd.add(Integer.toString(videoTranscoding.getResolution())); + cmd.add("--deinterlace"); + cmd.add("--crop"); + cmd.add("0:0:0:0"); + + Process process = null; + try { + if(log.isDebug()) { + log.debug(cmd.toString()); + } + ProcessBuilder builder = new ProcessBuilder(cmd); + process = builder.start(); + return updateVideoTranscodingFromProcessOutput(process, videoTranscoding, transcodedFile); + } catch (IOException e) { + log.error ("Could not spawn convert sub process", e); + return false; + } finally { + if (process != null) { + process.destroy(); + process = null; + } + } } + + /** + * Internal helper to deal with the handbrake console output and update the transcoding metadata + * @param proc + * @param videoTranscoding + * @param transcodedFile + * @return true: everything fine; false: an error happended somewhere + */ + private final boolean updateVideoTranscodingFromProcessOutput(Process proc, VideoTranscoding videoTranscoding, File transcodedFile) { + VideoManager videoManager = CoreSpringFactory.getImpl(VideoManager.class); + + StringBuilder errors = new StringBuilder(); + StringBuilder output = new StringBuilder(); + String line; + + // Read from standard input and parse percentages of transcoding process + InputStream stdout = proc.getInputStream(); + InputStreamReader isr = new InputStreamReader(stdout); + BufferedReader br = new BufferedReader(isr); + line = null; + try { + while ((line = br.readLine()) != null) { + output.append(line); + // Parse the percentage. Logline looks like this: + // Encoding: task 1 of 1, 85.90 % (307.59 fps, avg 330.35 fps, ETA 00h00m05s) + int start = line.indexOf(","); + if (start != -1) { + line = line.substring(start); + int end = line.indexOf("."); + if (end != -1 && end < 5) { + String percent = line.substring(2, end); + log.debug("Output: " + percent); + // update version file for UI + videoTranscoding.setStatus(Integer.parseInt(percent)); + videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); + DBFactory.getInstance().commitAndCloseSession(); + } + } + } + } catch (IOException e) { + // + } + + // Read and ignore errors, Handbrake outputs a lot info on startup. Only + // display errors in debug level + InputStream stderr = proc.getErrorStream(); + InputStreamReader iserr = new InputStreamReader(stderr); + BufferedReader berr = new BufferedReader(iserr); + line = null; + try { + while ((line = berr.readLine()) != null) { + errors.append(line); + log.debug("Error: " + line); + } + } catch (IOException e) { + // + } + + try { + // On finish, update metadata file + int exitValue = proc.waitFor(); + if (exitValue == 0) { + MovieService movieService = CoreSpringFactory.getImpl(MovieService.class); + Size videoSize = movieService.getSize(new LocalFileImpl(transcodedFile), VideoManagerImpl.FILETYPE_MP4); + videoTranscoding.setWidth(videoSize.getWidth()); + videoTranscoding.setHeight(videoSize.getHeight()); + videoTranscoding.setSize(transcodedFile.length()); + videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_DONE); + videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); + DBFactory.getInstance().commitAndCloseSession(); + return true; + } + return false; + } catch (InterruptedException e) { + return false; + } + } + + } diff --git a/src/main/java/org/olat/modules/video/manager/VideoTranscodingTask.java b/src/main/java/org/olat/modules/video/manager/VideoTranscodingTask.java deleted file mode 100644 index 737bce37bcd745ecebc51beff37faf045dc7ac6e..0000000000000000000000000000000000000000 --- a/src/main/java/org/olat/modules/video/manager/VideoTranscodingTask.java +++ /dev/null @@ -1,191 +0,0 @@ -/** - * <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.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; - -import org.olat.core.CoreSpringFactory; -import org.olat.core.commons.persistence.DBFactory; -import org.olat.core.commons.services.image.Size; -import org.olat.core.commons.services.taskexecutor.LongRunnable; -import org.olat.core.commons.services.taskexecutor.Sequential; -import org.olat.core.commons.services.video.MovieService; -import org.olat.core.logging.OLog; -import org.olat.core.logging.Tracing; -import org.olat.core.util.vfs.LocalFileImpl; -import org.olat.core.util.vfs.LocalFolderImpl; -import org.olat.modules.video.VideoManager; -import org.olat.modules.video.VideoModule; -import org.olat.modules.video.VideoTranscoding; -import org.olat.resource.OLATResource; - -/** - * This task implements transcoding of a single video file using the Handbrake CLI. - * - * Initial date: 22.04.2016<br> - * @author gnaegi, gnaegi@frentix.com, http://www.frentix.com - * - */ -public class VideoTranscodingTask implements LongRunnable, Sequential { - private static final long serialVersionUID = 2982868860465334552L; - private static final OLog log = Tracing.createLoggerFor(VideoTranscodingTask.class); - private OLATResource video; - private VideoTranscoding videoTranscoding; - private File transcodedFile; - - /** - * - * @param video - * @param version - */ - VideoTranscodingTask(OLATResource video, VideoTranscoding videoTranscoding) { - this.video = video; - this.videoTranscoding = videoTranscoding; - } - - - @Override - public void run() { - VideoModule videoModule = CoreSpringFactory.getImpl(VideoModule.class); - VideoManager videoManager = CoreSpringFactory.getImpl(VideoManager.class); - File masterFile = videoManager.getVideoFile(video); - File transcodingFolder = ((LocalFolderImpl)videoManager.getTranscodingContainer(video)).getBasefile(); - transcodedFile = new File(transcodingFolder, Integer.toString(videoTranscoding.getResolution()) + masterFile.getName()); - // mark this as beeing transcoded by this local transcoder - videoTranscoding.setTranscoder(VideoTranscoding.TRANSCODER_LOCAL); - videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); - - ArrayList<String> cmd = new ArrayList<>(); - String tasksetConfig = videoModule.getTranscodingTasksetConfig(); - if (tasksetConfig != null && !"Mac OS X".equals(System.getProperty("os.name"))) { - cmd.add("taskset"); - cmd.add("-c"); - cmd.add(tasksetConfig); - } - cmd.add("HandBrakeCLI"); - cmd.add("-i"); - cmd.add(masterFile.getAbsolutePath()); - cmd.add("-o"); - cmd.add(transcodedFile.getAbsolutePath()); - cmd.add("--optimize"); - cmd.add("--preset"); - cmd.add("Normal"); - cmd.add("--height"); - cmd.add(Integer.toString(videoTranscoding.getResolution())); - cmd.add("--deinterlace"); - cmd.add("--crop"); - cmd.add("0:0:0:0"); - - Process process = null; - try { - if(log.isDebug()) { - log.debug(cmd.toString()); - } - ProcessBuilder builder = new ProcessBuilder(cmd); - process = builder.start(); - executeProcess(process); - } catch (IOException e) { - log.error ("Could not spawn convert sub process", e); - if (process != null) { - process.destroy(); - process = null; - } - } - } - - - /** - * Internal helper to deal with the handbrake console output and update the transcoding metadata - * @param proc - */ - private final void executeProcess(Process proc) { - VideoManager videoManager = CoreSpringFactory.getImpl(VideoManager.class); - - StringBuilder errors = new StringBuilder(); - StringBuilder output = new StringBuilder(); - String line; - - // Read from standard input and parse percentages of transcoding process - InputStream stdout = proc.getInputStream(); - InputStreamReader isr = new InputStreamReader(stdout); - BufferedReader br = new BufferedReader(isr); - line = null; - try { - while ((line = br.readLine()) != null) { - output.append(line); - // Parse the percentage. Logline looks like this: - // Encoding: task 1 of 1, 85.90 % (307.59 fps, avg 330.35 fps, ETA 00h00m05s) - int start = line.indexOf(","); - if (start != -1) { - line = line.substring(start); - int end = line.indexOf("."); - if (end != -1 && end < 5) { - String percent = line.substring(2, end); - log.debug("Output: " + percent); - // update version file for UI - videoTranscoding.setStatus(Integer.parseInt(percent)); - videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); - DBFactory.getInstance().commitAndCloseSession(); - } - } - } - } catch (IOException e) { - // - } - - // Read and ignore errors, Handbrake outputs a lot info on startup. Only - // display errors in debug level - InputStream stderr = proc.getErrorStream(); - InputStreamReader iserr = new InputStreamReader(stderr); - BufferedReader berr = new BufferedReader(iserr); - line = null; - try { - while ((line = berr.readLine()) != null) { - errors.append(line); - log.debug("Error: " + line); - } - } catch (IOException e) { - // - } - - try { - // On finish, update metadata file - int exitValue = proc.waitFor(); - if (exitValue == 0) { - MovieService movieService = CoreSpringFactory.getImpl(MovieService.class); - Size videoSize = movieService.getSize(new LocalFileImpl(transcodedFile), VideoManagerImpl.FILETYPE_MP4); - videoTranscoding.setWidth(videoSize.getWidth()); - videoTranscoding.setHeight(videoSize.getHeight()); - videoTranscoding.setSize(transcodedFile.length()); - videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_DONE); - videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); - DBFactory.getInstance().commitAndCloseSession(); - } - } catch (InterruptedException e) { - // - //TODO: do I need to remove task from DB? - } - } -} diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties index 4989626c794e603c8d3a0b85526b2081f20a0934..a971e39f3655c7696185de0eb7734060b6918301 100644 --- a/src/main/resources/serviceconfig/olat.properties +++ b/src/main/resources/serviceconfig/olat.properties @@ -1165,22 +1165,35 @@ monitoring.instance.description=OpenOLAT instance monitored.probes=Runtime,System,Database,Memory,OpenOLAT,Release,Environment,Indexer monitoring.dependency.server=myserver -# Video resource +# Video resource and course node video.enabled=true video.coursenode.enabled=true -# The binary "HandBrakeCLI" must be installed to make transcoding work +# Enable transcoding if you want to create optimized version of your video for delivery. video.transcoding.enabled=false +# The transcoding process can run locally in s separate process using HandBrake. In that +# case, the binary "HandBrakeCLI" must be installed in your system and available to the +# java environment of your system. +# Alternatively, you can disable local transcoding and implement a shell script that runs +# on another server which reads from the o_vid_transcoding table to separate transcoding +# from the main server. For larger installations it is recommendet to run the transcoding +# on a dedicated server +video.transcoding.local=true +# Use tasklist to limit CPU usage if you set video.transcoding.local=true +video.transcoding.taskset.cpuconfig=0,1 +video.transcoding.taskset.cpuconfig.values=0,1 to indicate usage of 2 cores, set empty value to disable taskset (e.g. on osx not available) +# List of transcoding versions to be generated by the transcoder video.transcoding.resolutions=1080,720,480,360 video.transcoding.resolutions.values=2160,1080,720,480,360,240 # Where to store transcoded versions. This can be located on another path in case the # transcoding service is working on another physical server or you just want it to use # another (cheap) disk. By default it is also located in the olatdata dir. # The master video files are kept in the olatdata directory, this is only about the -# transcoded versions +# transcoded versions. +# If you set video.transcoding.local=false and use an external script for transcoding, this +# other script must have access to this video.transcoding.dir as well to store the transcoded +# videos. Make sure the access permissions are implemented in a way so that the OpenOLAT process +# can always delete the files from this video.transcoding.dir directory (when deleting the +# master video) video.transcoding.dir=${folder.root}/transcodedVideos video.transcoding.dir.values=${folder.root}/transcodedVideos, /mount/cheap/disk/transcodedVideos -# Use tasklist to limit CPU usage. -video.transcoding.taskset.cpuconfig=0,1 -video.transcoding.taskset.cpuconfig.values=0,1 to indicate usage of 2 cores, set empty value to disable taskset (e.g. on osx not available) - diff --git a/src/test/java/org/olat/modules/video/manager/VideoTranscodingDAOTest.java b/src/test/java/org/olat/modules/video/manager/VideoTranscodingDAOTest.java index 6619a10533cc4b344733615dc20250c82c9d69c3..5d81f4239a8506557faee9ac22930175861fea95 100644 --- a/src/test/java/org/olat/modules/video/manager/VideoTranscodingDAOTest.java +++ b/src/test/java/org/olat/modules/video/manager/VideoTranscodingDAOTest.java @@ -46,14 +46,34 @@ public class VideoTranscodingDAOTest extends OlatTestCase { @Test public void createVideoTranscoding() { OLATResource resource = JunitTestHelper.createRandomResource(); + // pending transcoding VideoTranscoding vTranscoding = videoTranscodingDao.createVideoTranscoding(resource, 1080, "mp4"); Assert.assertNotNull(vTranscoding); dbInstance.commitAndCloseSession(); - + + // done transcoding + VideoTranscoding vTranscoding2 = videoTranscodingDao.createVideoTranscoding(resource, 720, "mp4"); + Assert.assertNotNull(vTranscoding2); + vTranscoding2.setStatus(VideoTranscoding.TRANSCODING_STATUS_DONE); + vTranscoding2.setTranscoder(VideoTranscoding.TRANSCODER_LOCAL); + vTranscoding2 = videoTranscodingDao.updateTranscoding(vTranscoding2); + Assert.assertNotNull(vTranscoding2); + Assert.assertTrue(vTranscoding2.getStatus() == 100); + dbInstance.commitAndCloseSession(); + + // check for transcodings of resource List<VideoTranscoding> vTranscodingList = videoTranscodingDao.getVideoTranscodings(resource); Assert.assertNotNull(vTranscodingList); - Assert.assertEquals(1, vTranscodingList.size()); + Assert.assertEquals(2, vTranscodingList.size()); Assert.assertEquals(vTranscoding, vTranscodingList.get(0)); + Assert.assertEquals(vTranscoding2, vTranscodingList.get(1)); + + // check for overall pending transcodings + List<VideoTranscoding> vTranscodingList2 = videoTranscodingDao.getVideoTranscodingsPendingAndInProgress(); + Assert.assertNotNull(vTranscodingList2); + Assert.assertEquals(1, vTranscodingList2.size()); + Assert.assertEquals(vTranscoding, vTranscodingList2.get(0)); + } }