Java tutorial
/* * This file is part of Subsonic. * * Subsonic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Subsonic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Subsonic. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2016 (C) Sindre Mehus */ package net.sourceforge.subsonic.service; import java.awt.Dimension; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.FileFileFilter; import com.google.common.collect.Iterables; import net.sourceforge.subsonic.Logger; import net.sourceforge.subsonic.dao.VideoConversionDao; import net.sourceforge.subsonic.domain.MediaFile; import net.sourceforge.subsonic.domain.VideoConversion; import net.sourceforge.subsonic.service.metadata.MetaData; import net.sourceforge.subsonic.service.metadata.MetaDataParser; import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; import net.sourceforge.subsonic.service.metadata.Track; import net.sourceforge.subsonic.util.Util; /** * @author Sindre Mehus * @version $Id$ */ public class VideoConversionService { private static final Logger LOG = Logger.getLogger(VideoConversionService.class); private MediaFileService mediaFileService; private TranscodingService transcodingService; private SettingsService settingsService; private VideoConversionDao videoConversionDao; private VideoConverter videoConverter; private MetaDataParserFactory metaDataParserFactory; public void init() { videoConverter = new VideoConverter(); videoConverter.start(); for (VideoConversion conversion : videoConversionDao .getVideoConversionsByStatus(VideoConversion.Status.IN_PROGRESS)) { deleteVideoConversion(conversion); } } public void createVideoConversion(VideoConversion conversion) { videoConversionDao.deleteVideoConversionsForFile(conversion.getMediaFileId()); videoConversionDao.createVideoConversion(conversion); } public void deleteVideoConversion(VideoConversion conversion) { if (conversion.getTargetFile() != null) { File targetFile = new File(conversion.getTargetFile()); if (targetFile.exists()) { targetFile.delete(); } } if (conversion.getLogFile() != null) { File logFile = new File(conversion.getLogFile()); if (logFile.exists()) { logFile.delete(); } } videoConversionDao.deleteVideoConversion(conversion.getId()); videoConverter.cancel(conversion); } public VideoConversion getVideoConversionForFile(int mediaFileId) { VideoConversion conversion = videoConversionDao.getVideoConversionForFile(mediaFileId); if (conversion == null) { return null; } List<VideoConversion> conversions = deleteIfFileMissing(Collections.singletonList(conversion)); return Iterables.getFirst(conversions, null); } public List<VideoConversion> getAllVideoConversions() { return deleteIfFileMissing(videoConversionDao.getAllVideoConversions()); } public MetaData getVideoMetaData(MediaFile video) { MetaDataParser parser = metaDataParserFactory.getParser(video.getFile()); return parser != null ? parser.getMetaData(video.getFile()) : null; } private List<VideoConversion> deleteIfFileMissing(List<VideoConversion> conversions) { List<VideoConversion> result = new ArrayList<VideoConversion>(); for (VideoConversion conversion : conversions) { if (conversion.getStatus() == VideoConversion.Status.COMPLETED && (conversion.getTargetFile() == null || !(new File(conversion.getTargetFile()).exists()))) { deleteVideoConversion(conversion); } else { result.add(conversion); } } return result; } public void setVideoConversionDao(VideoConversionDao videoConversionDao) { this.videoConversionDao = videoConversionDao; } public void setMediaFileService(MediaFileService mediaFileService) { this.mediaFileService = mediaFileService; } public void setTranscodingService(TranscodingService transcodingService) { this.transcodingService = transcodingService; } public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { this.metaDataParserFactory = metaDataParserFactory; } public void setSettingsService(SettingsService settingsService) { this.settingsService = settingsService; } private class VideoConverter extends Thread { private VideoConversion conversion; private Process process; private MediaFile mediaFile; private VideoConverter() { setDaemon(true); } @Override public void run() { while (true) { Util.sleep(3000L); List<VideoConversion> conversions = videoConversionDao .getVideoConversionsByStatus(VideoConversion.Status.NEW); if (!conversions.isEmpty()) { conversion = conversions.get(0); convert(); } } } private void cancel(VideoConversion conversion) { if (process != null && this.conversion != null && this.conversion.getId().equals(conversion.getId())) { LOG.info("Killing conversion process for " + mediaFile); process.destroy(); } } private void convert() { videoConversionDao.updateStatus(conversion.getId(), VideoConversion.Status.IN_PROGRESS); mediaFile = mediaFileService.getMediaFile(conversion.getMediaFileId()); try { checkDiskLimit(); LOG.info("Starting video conversion of " + mediaFile); File logFile = new File(conversion.getLogFile()); File targetFile = new File(conversion.getTargetFile()); if (!targetFile.getParentFile().canWrite()) { throw new Exception("Write access denied to " + targetFile); } List<String> command = buildFFmpegCommand(targetFile); StringBuffer buf = new StringBuffer("Starting video converter: "); for (String s : command) { buf.append(s).append(" "); } LOG.info(buf); process = new ProcessBuilder(command).redirectErrorStream(true).start(); new ProcessReaderThread(process, conversion, logFile).start(); int retval = process.waitFor(); LOG.info("ffmpeg exit value: " + retval); boolean success = videoConversionDao.getVideoConversionById(conversion.getId()) != null // conversion was canceled (i.e., removed) && targetFile.exists() && targetFile.length() > 0; if (success) { LOG.info("Completed video conversion of " + mediaFile); videoConversionDao.updateStatus(conversion.getId(), VideoConversion.Status.COMPLETED); } else { LOG.error("An error occurred while converting video " + mediaFile + ". See log file " + logFile.getAbsolutePath()); videoConversionDao.updateStatus(conversion.getId(), VideoConversion.Status.ERROR); } } catch (Exception x) { LOG.error("An error occurred while converting video " + mediaFile + ": " + x, x); videoConversionDao.updateStatus(conversion.getId(), VideoConversion.Status.ERROR); } } private void checkDiskLimit() { int limitInGB = settingsService.getVideoConversionDiskLimit(); if (limitInGB == 0) { return; } long limitInBytes = limitInGB * FileUtils.ONE_GB; File dir = new File(settingsService.getVideoConversionDirectory()); if (!dir.canRead() || !dir.isDirectory()) { return; } List<File> files = new ArrayList<File>(); long usedBytes = 0; for (File file : dir.listFiles((FileFilter) FileFileFilter.FILE)) { files.add(file); usedBytes += file.length(); } if (usedBytes < limitInBytes) { return; } // Sort files by modification date. Collections.sort(files, new Comparator<File>() { @Override public int compare(File a, File b) { long lastModifiedA = a.lastModified(); long lastModifiedB = b.lastModified(); if (lastModifiedA < lastModifiedB) { return -1; } if (lastModifiedA > lastModifiedB) { return 1; } return 0; } }); // Delete files until we're below the limit. while (usedBytes > limitInBytes && !files.isEmpty()) { File victim = files.remove(0); usedBytes -= victim.length(); victim.delete(); LOG.info("Deleted converted video file " + victim); } } private List<String> buildFFmpegCommand(File targetFile) { List<String> command = new ArrayList<String>(); command.add(transcodingService.getTranscodeDirectory() + File.separator + "ffmpeg"); command.add("-i"); command.add(mediaFile.getFile().getAbsolutePath()); command.add("-ac"); command.add("2"); command.add("-f"); command.add("mp4"); command.add("-preset"); command.add("superfast"); command.add("-strict"); command.add("-2"); command.add("-y"); Integer bitRate = conversion.getBitRate(); if (bitRate != null) { command.add("-b:v"); command.add(bitRate + "k"); if (bitRate < 2000) { Dimension dim = Util.getSuitableVideoSize(mediaFile.getWidth(), mediaFile.getHeight(), bitRate); command.add("-s"); command.add(dim.width + "x" + dim.height); } } MetaData metaData = getVideoMetaData(mediaFile); Track videoTrack = null; List<Track> videoTracks = metaData.getVideoTracks(); if (bitRate == null) { // Look for video track with streamable codec. If found, copy it. for (Track track : videoTracks) { if (track.isStreamable()) { command.add("-c:v"); command.add("copy"); videoTrack = track; break; } } } if (videoTrack == null && !videoTracks.isEmpty()) { videoTrack = videoTracks.get(0); } // Find audio track, taking user-preferred track into account. List<Track> audioTracks = metaData.getAudioTracks(); Track audioTrack = null; if (conversion.getAudioTrackId() != null) { for (Track track : audioTracks) { if (conversion.getAudioTrackId().equals(track.getId())) { audioTrack = track; break; } } } if (audioTrack == null && !audioTracks.isEmpty()) { audioTrack = audioTracks.get(0); } // Copy audio track if streamable. if (audioTrack != null && audioTrack.isStreamable()) { command.add("-c:a"); command.add("copy"); } // If container has multiple audio or video tracks, add "-map" options to specify which // tracks to include. if (videoTracks.size() > 1 || audioTracks.size() > 1) { if (videoTrack != null) { command.add("-map"); command.add("0:" + videoTrack.getId()); } if (audioTrack != null) { command.add("-map"); command.add("0:" + audioTrack.getId()); } } command.add(targetFile.getAbsolutePath()); return command; } } private class ProcessReaderThread extends Thread { private final Process process; private final VideoConversion conversion; private final File logFile; public ProcessReaderThread(Process process, VideoConversion conversion, File logFile) { this.process = process; this.conversion = conversion; this.logFile = logFile; setDaemon(true); } @Override public void run() { // frame= 2558 fps=150 q=24.0 size= 82720kB time=00:01:47.47 bitrate=6305.3kbits/s dup=1 drop=0 Pattern pattern = Pattern.compile("^frame=.*time=(\\d+):(\\d+):(\\d+).(\\d+).*"); BufferedReader reader = null; BufferedWriter logWriter = null; try { reader = new BufferedReader(new InputStreamReader(process.getInputStream())); logWriter = new BufferedWriter(new FileWriter(logFile)); for (String line = reader.readLine(); line != null; line = reader.readLine()) { logWriter.append(line); logWriter.newLine(); logWriter.flush(); Matcher matcher = pattern.matcher(line); if (matcher.matches()) { int hours = Integer.parseInt(matcher.group(1)); int minutes = Integer.parseInt(matcher.group(2)); int seconds = Integer.parseInt(matcher.group(3)); int progress = hours * 3600 + minutes * 60 + seconds; videoConversionDao.updateProgress(conversion.getId(), progress); } } } catch (IOException e) { LOG.warn("Error when reading output from video converter.", e); } finally { IOUtils.closeQuietly(reader); IOUtils.closeQuietly(logWriter); } } } }