/**
* <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 java.util.List;
import org.hibernate.ObjectDeletedException;
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.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;
/**
*
* Initial date: 06.05.2016<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class VideoTranscodingJob extends JobWithDB implements StatefulJob {
/**
*
* @see org.olat.core.commons.services.scheduler.JobWithDB#executeWithDB(org.quartz.JobExecutionContext)
*/
@Override
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
try {
videoTranscoding.setStatus(Integer.parseInt(percent));
videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding);
DBFactory.getInstance().commitAndCloseSession();
} catch (ObjectDeletedException e) {
// deleted by other process
proc.destroy();
br.close();
return false;
}
}
}
}
} catch (IOException e) {
//
} finally {
try {
stdout.close();
isr.close();
br.close();
} catch (Exception e2) {
// ignore
}
}
// 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) {
//
} finally {
try {
stderr.close();
iserr.close();
berr.close();
} catch (Exception e2) {
// ignore
}
}
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;
}
}
}