aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/io
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/io')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java63
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java154
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java150
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java205
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java124
5 files changed, 696 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java
new file mode 100644
index 00000000..1019f73a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java
@@ -0,0 +1,63 @@
+/*
+ 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.io;
+
+import net.sourceforge.subsonic.*;
+import org.apache.commons.io.*;
+
+import java.io.*;
+
+/**
+ * Utility class which reads everything from an input stream and optionally logs it.
+ *
+ * @see TranscodeInputStream
+ * @author Sindre Mehus
+ */
+public class InputStreamReaderThread extends Thread {
+
+ private static final Logger LOG = Logger.getLogger(InputStreamReaderThread.class);
+
+ private InputStream input;
+ private String name;
+ private boolean log;
+
+ public InputStreamReaderThread(InputStream input, String name, boolean log) {
+ super(name + " InputStreamLogger");
+ this.input = input;
+ this.name = name;
+ this.log = log;
+ }
+
+ public void run() {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(input));
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ if (log) {
+ LOG.debug('(' + name + ") " + line);
+ }
+ }
+ } catch (IOException x) {
+ // Intentionally ignored.
+ } finally {
+ IOUtils.closeQuietly(reader);
+ IOUtils.closeQuietly(input);
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java
new file mode 100644
index 00000000..3be7fdd9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java
@@ -0,0 +1,154 @@
+/*
+ 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.io;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SearchService;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.service.AudioScrobblerService;
+import net.sourceforge.subsonic.service.TranscodingService;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Implementation of {@link InputStream} which reads from a {@link net.sourceforge.subsonic.domain.PlayQueue}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueueInputStream extends InputStream {
+
+ private static final Logger LOG = Logger.getLogger(PlayQueueInputStream.class);
+
+ private final Player player;
+ private final TransferStatus status;
+ private final Integer maxBitRate;
+ private final String preferredTargetFormat;
+ private final VideoTranscodingSettings videoTranscodingSettings;
+ private final TranscodingService transcodingService;
+ private final AudioScrobblerService audioScrobblerService;
+ private final MediaFileService mediaFileService;
+ private MediaFile currentFile;
+ private InputStream currentInputStream;
+ private SearchService searchService;
+
+ public PlayQueueInputStream(Player player, TransferStatus status, Integer maxBitRate, String preferredTargetFormat,
+ VideoTranscodingSettings videoTranscodingSettings, TranscodingService transcodingService,
+ AudioScrobblerService audioScrobblerService, MediaFileService mediaFileService, SearchService searchService) {
+ this.player = player;
+ this.status = status;
+ this.maxBitRate = maxBitRate;
+ this.preferredTargetFormat = preferredTargetFormat;
+ this.videoTranscodingSettings = videoTranscodingSettings;
+ this.transcodingService = transcodingService;
+ this.audioScrobblerService = audioScrobblerService;
+ this.mediaFileService = mediaFileService;
+ this.searchService = searchService;
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ int n = read(b);
+ return n == -1 ? -1 : b[0];
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ prepare();
+ if (currentInputStream == null || player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
+ return -1;
+ }
+
+ int n = currentInputStream.read(b, off, len);
+
+ // If end of song reached, skip to next song and call read() again.
+ if (n == -1) {
+ player.getPlayQueue().next();
+ close();
+ return read(b, off, len);
+ } else {
+ status.addBytesTransfered(n);
+ }
+ return n;
+ }
+
+ private void prepare() throws IOException {
+ PlayQueue playQueue = player.getPlayQueue();
+
+ // If playlist is in auto-random mode, populate it with new random songs.
+ if (playQueue.getIndex() == -1 && playQueue.getRandomSearchCriteria() != null) {
+ populateRandomPlaylist(playQueue);
+ }
+
+ MediaFile result;
+ synchronized (playQueue) {
+ result = playQueue.getCurrentFile();
+ }
+ MediaFile file = result;
+ if (file == null) {
+ close();
+ } else if (!file.equals(currentFile)) {
+ close();
+ LOG.info(player.getUsername() + " listening to \"" + FileUtil.getShortPath(file.getFile()) + "\"");
+ mediaFileService.incrementPlayCount(file);
+ if (player.getClientId() == null) { // Don't scrobble REST players.
+ audioScrobblerService.register(file, player.getUsername(), false);
+ }
+
+ TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings);
+ currentInputStream = transcodingService.getTranscodedInputStream(parameters);
+ currentFile = file;
+ status.setFile(currentFile.getFile());
+ }
+ }
+
+ private void populateRandomPlaylist(PlayQueue playQueue) throws IOException {
+ List<MediaFile> files = searchService.getRandomSongs(playQueue.getRandomSearchCriteria());
+ playQueue.addFiles(false, files);
+ LOG.info("Recreated random playlist with " + playQueue.size() + " songs.");
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ if (currentInputStream != null) {
+ currentInputStream.close();
+ }
+ } finally {
+ if (player.getClientId() == null) { // Don't scrobble REST players.
+ audioScrobblerService.register(currentFile, player.getUsername(), true);
+ }
+ currentInputStream = null;
+ currentFile = null;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java
new file mode 100644
index 00000000..25bc03d2
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java
@@ -0,0 +1,150 @@
+/*
+ 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.io;
+
+import org.apache.commons.lang.math.Range;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+/**
+ * Special output stream for grabbing only part of a passed stream.
+ *
+ * @author Sindre Mehus (based on code found on http://www.koders.com/
+ */
+public class RangeOutputStream extends FilterOutputStream {
+
+ /**
+ * The starting index.
+ */
+ private long start;
+
+ /**
+ * The ending index.
+ */
+ private long end;
+
+ /**
+ * The current position.
+ */
+ protected long pos;
+
+ /**
+ * Wraps the given output stream in a RangeOutputStream, using the values
+ * in the given range, unless the range is <code>null</code> in which case
+ * the original OutputStream is returned.
+ *
+ * @param out The output stream to wrap in a RangeOutputStream.
+ * @param range The range, may be <code>null</code>.
+ * @return The possibly wrapped output stream.
+ */
+ public static OutputStream wrap(OutputStream out, Range range) {
+ if (range == null) {
+ return out;
+ }
+ return new RangeOutputStream(out, range.getMinimumLong(), range.getMaximumLong());
+ }
+
+ /**
+ * Creates the stream with the passed start and end.
+ *
+ * @param out The stream to write to.
+ * @param start The starting position.
+ * @param end The ending position.
+ */
+ public RangeOutputStream(OutputStream out, long start, long end) {
+ super(out);
+ this.start = start;
+ this.end = end;
+ pos = 0;
+ }
+
+ /**
+ * Writes the byte IF it is within the range, otherwise it only
+ * increments the position.
+ *
+ * @param b The byte to write.
+ * @throws IOException Thrown if there was a problem writing to the stream.
+ */
+ @Override
+ public void write(int b) throws IOException {
+ if ((pos >= start) && (pos <= end)) {
+ super.write(b);
+ }
+ pos++;
+ }
+
+ /**
+ * Writes the bytes IF it is within the range, otherwise it only
+ * increments the position.
+ *
+ * @param b The bytes to write.
+ * @param off The offset to start at.
+ * @param len The length to write.
+ * @throws IOException Thrown if there was a problem writing to the stream.
+ */
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ boolean allowWrite = false;
+ long newPos = pos + off, newOff = off, newLen = len;
+
+ // Check to see if we are in the range
+ if (newPos <= end) {
+ if (newPos >= start) {
+ // We are so check to make sure we don't leave it
+ if (newPos + newLen > end) {
+ newLen = end - newPos;
+ }
+
+ // Enable writing
+ allowWrite = true;
+ }
+
+ // We aren't yet in the range, but if see if the proposed write
+ // would place us there
+ else if (newPos + newLen >= start) {
+ // It would so, update the offset
+ newOff += start - newPos;
+
+ // New offset means, a new position, so update that too
+ newPos = newOff + pos;
+ newLen = len + (pos - newPos);
+
+ // Make sure we don't go past the range
+ if (newPos + newLen > end) {
+ newLen = end - newPos;
+ }
+
+ // Enable writting
+ allowWrite = true;
+ }
+ }
+
+ // If we have enabled writing, do the write!
+ if (allowWrite) {
+ out.write(b, (int) newOff, (int) newLen);
+ }
+
+ // Move the cursor along
+ pos += off + len;
+ }
+}
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java
new file mode 100644
index 00000000..9a8618c6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java
@@ -0,0 +1,205 @@
+/*
+ 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.io;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Implements SHOUTcast support by decorating an existing output stream.
+ * <p/>
+ * Based on protocol description found on
+ * <em>http://www.smackfu.com/stuff/programming/shoutcast.html</em>
+ *
+ * @author Sindre Mehus
+ */
+public class ShoutCastOutputStream extends OutputStream {
+
+ private static final Logger LOG = Logger.getLogger(ShoutCastOutputStream.class);
+
+ /**
+ * Number of bytes between each SHOUTcast metadata block.
+ */
+ public static final int META_DATA_INTERVAL = 20480;
+
+ /**
+ * The underlying output stream to decorate.
+ */
+ private OutputStream out;
+
+ /**
+ * What to write in the SHOUTcast metadata is fetched from the playlist.
+ */
+ private PlayQueue playQueue;
+
+ /**
+ * Keeps track of the number of bytes written (excluding meta-data). Between 0 and {@link #META_DATA_INTERVAL}.
+ */
+ private int byteCount;
+
+ /**
+ * The last stream title sent.
+ */
+ private String previousStreamTitle;
+
+ private SettingsService settingsService;
+
+ /**
+ * Creates a new SHOUTcast-decorated stream for the given output stream.
+ *
+ * @param out The output stream to decorate.
+ * @param playQueue Meta-data is fetched from this playlist.
+ */
+ public ShoutCastOutputStream(OutputStream out, PlayQueue playQueue, SettingsService settingsService) {
+ this.out = out;
+ this.playQueue = playQueue;
+ this.settingsService = settingsService;
+ }
+
+ /**
+ * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(byte[] b, int off, int len) throws IOException {
+
+ int bytesWritten = 0;
+ while (bytesWritten < len) {
+
+ // 'n' is the number of bytes to write before the next potential meta-data block.
+ int n = Math.min(len - bytesWritten, ShoutCastOutputStream.META_DATA_INTERVAL - byteCount);
+
+ out.write(b, off + bytesWritten, n);
+ bytesWritten += n;
+ byteCount += n;
+
+ // Reached meta-data block?
+ if (byteCount % ShoutCastOutputStream.META_DATA_INTERVAL == 0) {
+ writeMetaData();
+ byteCount = 0;
+ }
+ }
+ }
+
+ /**
+ * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /**
+ * Writes the given byte to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(int b) throws IOException {
+ byte[] buf = new byte[]{(byte) b};
+ write(buf);
+ }
+
+ /**
+ * Flushes the underlying stream.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ /**
+ * Closes the underlying stream.
+ */
+ public void close() throws IOException {
+ out.close();
+ }
+
+ private void writeMetaData() throws IOException {
+ String streamTitle = StringUtils.trimToEmpty(settingsService.getWelcomeTitle());
+
+ MediaFile result;
+ synchronized (playQueue) {
+ result = playQueue.getCurrentFile();
+ }
+ MediaFile mediaFile = result;
+ if (mediaFile != null) {
+ streamTitle = mediaFile.getArtist() + " - " + mediaFile.getTitle();
+ }
+
+ byte[] bytes;
+
+ if (streamTitle.equals(previousStreamTitle)) {
+ bytes = new byte[0];
+ } else {
+ try {
+ previousStreamTitle = streamTitle;
+ bytes = createStreamTitle(streamTitle);
+ } catch (UnsupportedEncodingException x) {
+ LOG.warn("Failed to create SHOUTcast meta-data. Ignoring.", x);
+ bytes = new byte[0];
+ }
+ }
+
+ // Length in groups of 16 bytes.
+ int length = bytes.length / 16;
+ if (bytes.length % 16 > 0) {
+ length++;
+ }
+
+ // Write the length as a single byte.
+ out.write(length);
+
+ // Write the message.
+ out.write(bytes);
+
+ // Write padding zero bytes.
+ int padding = length * 16 - bytes.length;
+ for (int i = 0; i < padding; i++) {
+ out.write(0);
+ }
+ }
+
+ private byte[] createStreamTitle(String title) throws UnsupportedEncodingException {
+ // Remove any quotes from the title.
+ title = title.replaceAll("'", "");
+
+ // Convert non-ascii characters to similar ascii characters.
+ for (char[] chars : ShoutCastOutputStream.CHAR_MAP) {
+ title = title.replace(chars[0], chars[1]);
+ }
+
+ title = "StreamTitle='" + title + "';";
+ return title.getBytes("US-ASCII");
+ }
+
+ /**
+ * Maps from miscellaneous accented characters to similar-looking ASCII characters.
+ */
+ private static final char[][] CHAR_MAP = {
+ {'\u00C0', 'A'}, {'\u00C1', 'A'}, {'\u00C2', 'A'}, {'\u00C3', 'A'}, {'\u00C4', 'A'}, {'\u00C5', 'A'}, {'\u00C6', 'A'},
+ {'\u00C8', 'E'}, {'\u00C9', 'E'}, {'\u00CA', 'E'}, {'\u00CB', 'E'}, {'\u00CC', 'I'}, {'\u00CD', 'I'}, {'\u00CE', 'I'},
+ {'\u00CF', 'I'}, {'\u00D2', 'O'}, {'\u00D3', 'O'}, {'\u00D4', 'O'}, {'\u00D5', 'O'}, {'\u00D6', 'O'}, {'\u00D9', 'U'},
+ {'\u00DA', 'U'}, {'\u00DB', 'U'}, {'\u00DC', 'U'}, {'\u00DF', 'B'}, {'\u00E0', 'a'}, {'\u00E1', 'a'}, {'\u00E2', 'a'},
+ {'\u00E3', 'a'}, {'\u00E4', 'a'}, {'\u00E5', 'a'}, {'\u00E6', 'a'}, {'\u00E7', 'c'}, {'\u00E8', 'e'}, {'\u00E9', 'e'},
+ {'\u00EA', 'e'}, {'\u00EB', 'e'}, {'\u00EC', 'i'}, {'\u00ED', 'i'}, {'\u00EE', 'i'}, {'\u00EF', 'i'}, {'\u00F1', 'n'},
+ {'\u00F2', 'o'}, {'\u00F3', 'o'}, {'\u00F4', 'o'}, {'\u00F5', 'o'}, {'\u00F6', 'o'}, {'\u00F8', 'o'}, {'\u00F9', 'u'},
+ {'\u00FA', 'u'}, {'\u00FB', 'u'}, {'\u00FC', 'u'}, {'\u2013', '-'}
+ };
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java
new file mode 100644
index 00000000..b7a5e31e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java
@@ -0,0 +1,124 @@
+/*
+ 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.io;
+
+import net.sourceforge.subsonic.*;
+
+import org.apache.commons.io.*;
+
+import java.io.*;
+
+/**
+ * Subclass of {@link InputStream} which provides on-the-fly transcoding.
+ * Instances of <code>TranscodeInputStream</code> can be chained together, for instance to convert
+ * from OGG to WAV to MP3.
+ *
+ * @author Sindre Mehus
+ */
+public class TranscodeInputStream extends InputStream {
+
+ private static final Logger LOG = Logger.getLogger(TranscodeInputStream.class);
+
+ private InputStream processInputStream;
+ private OutputStream processOutputStream;
+ private Process process;
+ private final File tmpFile;
+
+ /**
+ * Creates a transcoded input stream by executing an external process. If <code>in</code> is not null,
+ * data from it is copied to the command.
+ *
+ * @param processBuilder Used to create the external process.
+ * @param in Data to feed to the process. May be {@code null}.
+ * @param tmpFile Temporary file to delete when this stream is closed. May be {@code null}.
+ * @throws IOException If an I/O error occurs.
+ */
+ public TranscodeInputStream(ProcessBuilder processBuilder, final InputStream in, File tmpFile) throws IOException {
+ this.tmpFile = tmpFile;
+
+ StringBuffer buf = new StringBuffer("Starting transcoder: ");
+ for (String s : processBuilder.command()) {
+ buf.append('[').append(s).append("] ");
+ }
+ LOG.debug(buf);
+
+ process = processBuilder.start();
+ processOutputStream = process.getOutputStream();
+ processInputStream = process.getInputStream();
+
+ // Must read stderr from the process, otherwise it may block.
+ final String name = processBuilder.command().get(0);
+ new InputStreamReaderThread(process.getErrorStream(), name, true).start();
+
+ // Copy data in a separate thread
+ if (in != null) {
+ new Thread(name + " TranscodedInputStream copy thread") {
+ public void run() {
+ try {
+ IOUtils.copy(in, processOutputStream);
+ } catch (IOException x) {
+ // Intentionally ignored. Will happen if the remote player closes the stream.
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(processOutputStream);
+ }
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * @see InputStream#read()
+ */
+ public int read() throws IOException {
+ return processInputStream.read();
+ }
+
+ /**
+ * @see InputStream#read(byte[])
+ */
+ public int read(byte[] b) throws IOException {
+ return processInputStream.read(b);
+ }
+
+ /**
+ * @see InputStream#read(byte[], int, int)
+ */
+ public int read(byte[] b, int off, int len) throws IOException {
+ return processInputStream.read(b, off, len);
+ }
+
+ /**
+ * @see InputStream#close()
+ */
+ public void close() throws IOException {
+ IOUtils.closeQuietly(processInputStream);
+ IOUtils.closeQuietly(processOutputStream);
+
+ if (process != null) {
+ process.destroy();
+ }
+
+ if (tmpFile != null) {
+ if (!tmpFile.delete()) {
+ LOG.warn("Failed to delete tmp file: " + tmpFile);
+ }
+ }
+ }
+}