aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
committerScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
commita1a18f77a50804e0127dfa4b0f5240c49c541184 (patch)
tree19a38880afe505beddb5590379a8134d7730a277 /subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
parentb61d787706979e7e20f4c3c4f93c1f129d92273f (diff)
downloaddsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip
Initial Commit
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java530
1 files changed, 530 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
new file mode 100644
index 00000000..2c8b9c5e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
@@ -0,0 +1,530 @@
+/*
+ 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 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.controller.VideoPlayerController;
+import net.sourceforge.subsonic.dao.TranscodingDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.io.TranscodeInputStream;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.filefilter.PrefixFileFilter;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Provides services for transcoding media. Transcoding is the process of
+ * converting an audio stream to a different format and/or bit rate. The latter is
+ * also called downsampling.
+ *
+ * @author Sindre Mehus
+ * @see TranscodeInputStream
+ */
+public class TranscodingService {
+
+ private static final Logger LOG = Logger.getLogger(TranscodingService.class);
+
+ private TranscodingDao transcodingDao;
+ private SettingsService settingsService;
+ private PlayerService playerService;
+
+ /**
+ * Returns all transcodings.
+ *
+ * @return Possibly empty list of all transcodings.
+ */
+ public List<Transcoding> getAllTranscodings() {
+ return transcodingDao.getAllTranscodings();
+ }
+
+ /**
+ * Returns all active transcodings for the given player. Only enabled transcodings are returned.
+ *
+ * @param player The player.
+ * @return All active transcodings for the player.
+ */
+ public List<Transcoding> getTranscodingsForPlayer(Player player) {
+ return transcodingDao.getTranscodingsForPlayer(player.getId());
+ }
+
+ /**
+ * Sets the list of active transcodings for the given player.
+ *
+ * @param player The player.
+ * @param transcodingIds ID's of the active transcodings.
+ */
+ public void setTranscodingsForPlayer(Player player, int[] transcodingIds) {
+ transcodingDao.setTranscodingsForPlayer(player.getId(), transcodingIds);
+ }
+
+ /**
+ * Sets the list of active transcodings for the given player.
+ *
+ * @param player The player.
+ * @param transcodings The active transcodings.
+ */
+ public void setTranscodingsForPlayer(Player player, List<Transcoding> transcodings) {
+ int[] transcodingIds = new int[transcodings.size()];
+ for (int i = 0; i < transcodingIds.length; i++) {
+ transcodingIds[i] = transcodings.get(i).getId();
+ }
+ setTranscodingsForPlayer(player, transcodingIds);
+ }
+
+
+ /**
+ * Creates a new transcoding.
+ *
+ * @param transcoding The transcoding to create.
+ */
+ public void createTranscoding(Transcoding transcoding) {
+ transcodingDao.createTranscoding(transcoding);
+
+ // Activate this transcoding for all players?
+ if (transcoding.isDefaultActive()) {
+ for (Player player : playerService.getAllPlayers()) {
+ List<Transcoding> transcodings = getTranscodingsForPlayer(player);
+ transcodings.add(transcoding);
+ setTranscodingsForPlayer(player, transcodings);
+ }
+ }
+ }
+
+ /**
+ * Deletes the transcoding with the given ID.
+ *
+ * @param id The transcoding ID.
+ */
+ public void deleteTranscoding(Integer id) {
+ transcodingDao.deleteTranscoding(id);
+ }
+
+ /**
+ * Updates the given transcoding.
+ *
+ * @param transcoding The transcoding to update.
+ */
+ public void updateTranscoding(Transcoding transcoding) {
+ transcodingDao.updateTranscoding(transcoding);
+ }
+
+ /**
+ * Returns whether transcoding is required for the given media file and player combination.
+ *
+ * @param mediaFile The media file.
+ * @param player The player.
+ * @return Whether transcoding will be performed if invoking the
+ * {@link #getTranscodedInputStream} method with the same arguments.
+ */
+ public boolean isTranscodingRequired(MediaFile mediaFile, Player player) {
+ return getTranscoding(mediaFile, player, null) != null;
+ }
+
+ /**
+ * Returns the suffix for the given player and media file, taking transcodings into account.
+ *
+ * @param player The player in question.
+ * @param file The media file.
+ * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}.
+ * @return The file suffix, e.g., "mp3".
+ */
+ public String getSuffix(Player player, MediaFile file, String preferredTargetFormat) {
+ Transcoding transcoding = getTranscoding(file, player, preferredTargetFormat);
+ return transcoding != null ? transcoding.getTargetFormat() : file.getFormat();
+ }
+
+ /**
+ * Creates parameters for a possibly transcoded or downsampled input stream for the given media file and player combination.
+ * <p/>
+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the
+ * given player.
+ * <p/>
+ * If no transcoding is applicable, the file may still be downsampled, given that the player is configured
+ * with a bit rate limit which is higher than the actual bit rate of the file.
+ * <p/>
+ * Otherwise, a normal input stream to the original file is returned.
+ *
+ * @param mediaFile The media file.
+ * @param player The player.
+ * @param maxBitRate Overrides the per-player and per-user bitrate limit. May be {@code null}.
+ * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}.
+ * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}.
+ * @return Parameters to be used in the {@link #getTranscodedInputStream} method.
+ */
+ public Parameters getParameters(MediaFile mediaFile, Player player, Integer maxBitRate, String preferredTargetFormat,
+ VideoTranscodingSettings videoTranscodingSettings) {
+
+ Parameters parameters = new Parameters(mediaFile, videoTranscodingSettings);
+
+ TranscodeScheme transcodeScheme = getTranscodeScheme(player);
+ if (maxBitRate == null && transcodeScheme != TranscodeScheme.OFF) {
+ maxBitRate = transcodeScheme.getMaxBitRate();
+ }
+
+ Transcoding transcoding = getTranscoding(mediaFile, player, preferredTargetFormat);
+ if (transcoding != null) {
+ parameters.setTranscoding(transcoding);
+ if (maxBitRate == null) {
+ maxBitRate = mediaFile.isVideo() ? VideoPlayerController.DEFAULT_BIT_RATE : 128;
+ }
+ } else if (maxBitRate != null) {
+ boolean supported = isDownsamplingSupported(mediaFile);
+ Integer bitRate = mediaFile.getBitRate();
+ if (supported && bitRate != null && bitRate > maxBitRate) {
+ parameters.setDownsample(true);
+ }
+ }
+
+ parameters.setMaxBitRate(maxBitRate);
+ return parameters;
+ }
+
+ /**
+ * Returns a possibly transcoded or downsampled input stream for the given music file and player combination.
+ * <p/>
+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the
+ * given player.
+ * <p/>
+ * If no transcoding is applicable, the file may still be downsampled, given that the player is configured
+ * with a bit rate limit which is higher than the actual bit rate of the file.
+ * <p/>
+ * Otherwise, a normal input stream to the original file is returned.
+ *
+ * @param parameters As returned by {@link #getParameters}.
+ * @return A possible transcoded or downsampled input stream.
+ * @throws IOException If an I/O error occurs.
+ */
+ public InputStream getTranscodedInputStream(Parameters parameters) throws IOException {
+ try {
+
+ if (parameters.getTranscoding() != null) {
+ return createTranscodedInputStream(parameters);
+ }
+
+ if (parameters.downsample) {
+ return createDownsampledInputStream(parameters);
+ }
+
+ } catch (Exception x) {
+ LOG.warn("Failed to transcode " + parameters.getMediaFile() + ". Using original.", x);
+ }
+
+ return new FileInputStream(parameters.getMediaFile().getFile());
+ }
+
+
+ /**
+ * Returns the strictest transcoding scheme defined for the player and the user.
+ */
+ private TranscodeScheme getTranscodeScheme(Player player) {
+ String username = player.getUsername();
+ if (username != null) {
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ return player.getTranscodeScheme().strictest(userSettings.getTranscodeScheme());
+ }
+
+ return player.getTranscodeScheme();
+ }
+
+ /**
+ * Returns an input stream by applying the given transcoding to the given music file.
+ *
+ * @param parameters Transcoding parameters.
+ * @return The transcoded input stream.
+ * @throws IOException If an I/O error occurs.
+ */
+ private InputStream createTranscodedInputStream(Parameters parameters)
+ throws IOException {
+
+ Transcoding transcoding = parameters.getTranscoding();
+ Integer maxBitRate = parameters.getMaxBitRate();
+ VideoTranscodingSettings videoTranscodingSettings = parameters.getVideoTranscodingSettings();
+ MediaFile mediaFile = parameters.getMediaFile();
+
+ TranscodeInputStream in = createTranscodeInputStream(transcoding.getStep1(), maxBitRate, videoTranscodingSettings, mediaFile, null);
+
+ if (transcoding.getStep2() != null) {
+ in = createTranscodeInputStream(transcoding.getStep2(), maxBitRate, videoTranscodingSettings, mediaFile, in);
+ }
+
+ if (transcoding.getStep3() != null) {
+ in = createTranscodeInputStream(transcoding.getStep3(), maxBitRate, videoTranscodingSettings, mediaFile, in);
+ }
+
+ return in;
+ }
+
+ /**
+ * Creates a transcoded input stream by interpreting the given command line string.
+ * This includes the following:
+ * <ul>
+ * <li>Splitting the command line string to an array.</li>
+ * <li>Replacing occurrences of "%s" with the path of the given music file.</li>
+ * <li>Replacing occurrences of "%t" with the title of the given music file.</li>
+ * <li>Replacing occurrences of "%l" with the album name of the given music file.</li>
+ * <li>Replacing occurrences of "%a" with the artist name of the given music file.</li>
+ * <li>Replacing occurrcences of "%b" with the max bitrate.</li>
+ * <li>Replacing occurrcences of "%o" with the video time offset (used for scrubbing).</li>
+ * <li>Replacing occurrcences of "%w" with the video image width.</li>
+ * <li>Replacing occurrcences of "%h" with the video image height.</li>
+ * <li>Prepending the path of the transcoder directory if the transcoder is found there.</li>
+ * </ul>
+ *
+ * @param command The command line string.
+ * @param maxBitRate The maximum bitrate to use. May not be {@code null}.
+ * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}.
+ * @param mediaFile The media file.
+ * @param in Data to feed to the process. May be {@code null}. @return The newly created input stream.
+ */
+ private TranscodeInputStream createTranscodeInputStream(String command, Integer maxBitRate,
+ VideoTranscodingSettings videoTranscodingSettings, MediaFile mediaFile, InputStream in) throws IOException {
+
+ String title = mediaFile.getTitle();
+ String album = mediaFile.getAlbumName();
+ String artist = mediaFile.getArtist();
+
+ if (title == null) {
+ title = "Unknown Song";
+ }
+ if (album == null) {
+ title = "Unknown Album";
+ }
+ if (artist == null) {
+ title = "Unknown Artist";
+ }
+
+ List<String> result = new LinkedList<String>(Arrays.asList(StringUtil.split(command)));
+ result.set(0, getTranscodeDirectory().getPath() + File.separatorChar + result.get(0));
+
+ File tmpFile = null;
+
+ for (int i = 1; i < result.size(); i++) {
+ String cmd = result.get(i);
+ if (cmd.contains("%b")) {
+ cmd = cmd.replace("%b", String.valueOf(maxBitRate));
+ }
+ if (cmd.contains("%t")) {
+ cmd = cmd.replace("%t", title);
+ }
+ if (cmd.contains("%l")) {
+ cmd = cmd.replace("%l", album);
+ }
+ if (cmd.contains("%a")) {
+ cmd = cmd.replace("%a", artist);
+ }
+ if (cmd.contains("%o") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%o", String.valueOf(videoTranscodingSettings.getTimeOffset()));
+ }
+ if (cmd.contains("%w") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%w", String.valueOf(videoTranscodingSettings.getWidth()));
+ }
+ if (cmd.contains("%h") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%h", String.valueOf(videoTranscodingSettings.getHeight()));
+ }
+ if (cmd.contains("%s")) {
+
+ // Work-around for filename character encoding problem on Windows.
+ // Create temporary file, and feed this to the transcoder.
+ String path = mediaFile.getFile().getAbsolutePath();
+ if (Util.isWindows() && !mediaFile.isVideo() && !StringUtils.isAsciiPrintable(path)) {
+ tmpFile = File.createTempFile("subsonic", "." + FilenameUtils.getExtension(path));
+ tmpFile.deleteOnExit();
+ FileUtils.copyFile(new File(path), tmpFile);
+ LOG.debug("Created tmp file: " + tmpFile);
+ cmd = cmd.replace("%s", tmpFile.getPath());
+ } else {
+ cmd = cmd.replace("%s", path);
+ }
+ }
+
+ result.set(i, cmd);
+ }
+ return new TranscodeInputStream(new ProcessBuilder(result), in, tmpFile);
+ }
+
+ /**
+ * Returns an applicable transcoding for the given file and player, or <code>null</code> if no
+ * transcoding should be done.
+ */
+ private Transcoding getTranscoding(MediaFile mediaFile, Player player, String preferredTargetFormat) {
+
+ List<Transcoding> applicableTranscodings = new LinkedList<Transcoding>();
+ String suffix = mediaFile.getFormat();
+
+ for (Transcoding transcoding : getTranscodingsForPlayer(player)) {
+ for (String sourceFormat : transcoding.getSourceFormatsAsArray()) {
+ if (sourceFormat.equalsIgnoreCase(suffix)) {
+ if (isTranscodingInstalled(transcoding)) {
+ applicableTranscodings.add(transcoding);
+ }
+ }
+ }
+ }
+
+ if (applicableTranscodings.isEmpty()) {
+ return null;
+ }
+
+ for (Transcoding transcoding : applicableTranscodings) {
+ if (transcoding.getTargetFormat().equalsIgnoreCase(preferredTargetFormat)) {
+ return transcoding;
+ }
+ }
+
+ return applicableTranscodings.get(0);
+ }
+
+ /**
+ * Returns a downsampled input stream to the music file.
+ *
+ * @param parameters Downsample parameters.
+ * @throws IOException If an I/O error occurs.
+ */
+ private InputStream createDownsampledInputStream(Parameters parameters) throws IOException {
+ String command = settingsService.getDownsamplingCommand();
+ return createTranscodeInputStream(command, parameters.getMaxBitRate(), parameters.getVideoTranscodingSettings(),
+ parameters.getMediaFile(), null);
+ }
+
+ /**
+ * Returns whether downsampling is supported (i.e., whether LAME is installed or not.)
+ *
+ * @param mediaFile If not null, returns whether downsampling is supported for this file.
+ * @return Whether downsampling is supported.
+ */
+ public boolean isDownsamplingSupported(MediaFile mediaFile) {
+ if (mediaFile != null) {
+ boolean isMp3 = "mp3".equalsIgnoreCase(mediaFile.getFormat());
+ if (!isMp3) {
+ return false;
+ }
+ }
+
+ String commandLine = settingsService.getDownsamplingCommand();
+ return isTranscodingStepInstalled(commandLine);
+ }
+
+ private boolean isTranscodingInstalled(Transcoding transcoding) {
+ return isTranscodingStepInstalled(transcoding.getStep1()) &&
+ isTranscodingStepInstalled(transcoding.getStep2()) &&
+ isTranscodingStepInstalled(transcoding.getStep3());
+ }
+
+ private boolean isTranscodingStepInstalled(String step) {
+ if (StringUtils.isEmpty(step)) {
+ return true;
+ }
+ String executable = StringUtil.split(step)[0];
+ PrefixFileFilter filter = new PrefixFileFilter(executable);
+ String[] matches = getTranscodeDirectory().list(filter);
+ return matches != null && matches.length > 0;
+ }
+
+ /**
+ * Returns the directory in which all transcoders are installed.
+ */
+ public File getTranscodeDirectory() {
+ File dir = new File(SettingsService.getSubsonicHome(), "transcode");
+ if (!dir.exists()) {
+ boolean ok = dir.mkdir();
+ if (ok) {
+ LOG.info("Created directory " + dir);
+ } else {
+ LOG.warn("Failed to create directory " + dir);
+ }
+ }
+ return dir;
+ }
+
+ public void setTranscodingDao(TranscodingDao transcodingDao) {
+ this.transcodingDao = transcodingDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public static class Parameters {
+ private boolean downsample;
+ private final MediaFile mediaFile;
+ private final VideoTranscodingSettings videoTranscodingSettings;
+ private Integer maxBitRate;
+ private Transcoding transcoding;
+
+ public Parameters(MediaFile mediaFile, VideoTranscodingSettings videoTranscodingSettings) {
+ this.mediaFile = mediaFile;
+ this.videoTranscodingSettings = videoTranscodingSettings;
+ }
+
+ public void setMaxBitRate(Integer maxBitRate) {
+ this.maxBitRate = maxBitRate;
+ }
+
+ public boolean isDownsample() {
+ return downsample;
+ }
+
+ public void setDownsample(boolean downsample) {
+ this.downsample = downsample;
+ }
+
+ public boolean isTranscode() {
+ return transcoding != null;
+ }
+
+ public void setTranscoding(Transcoding transcoding) {
+ this.transcoding = transcoding;
+ }
+
+ public Transcoding getTranscoding() {
+ return transcoding;
+ }
+
+ public MediaFile getMediaFile() {
+ return mediaFile;
+ }
+
+ public Integer getMaxBitRate() {
+ return maxBitRate;
+ }
+
+ public VideoTranscodingSettings getVideoTranscodingSettings() {
+ return videoTranscodingSettings;
+ }
+ }
+}