/* This file is part of Airsonic. Airsonic 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. Airsonic 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 Airsonic. If not, see . Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.io; import org.airsonic.player.util.FileUtil; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Subclass of {@link InputStream} which provides on-the-fly transcoding. * Instances of TranscodeInputStream 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 = LoggerFactory.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 in 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; StringBuilder buf = new StringBuilder("Starting transcoder: "); for (String s : processBuilder.command()) { buf.append('[').append(s).append("] "); } LOG.info(buf.toString()); 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 { FileUtil.closeQuietly(in); FileUtil.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() { FileUtil.closeQuietly(processInputStream); FileUtil.closeQuietly(processOutputStream); if (process != null) { process.destroy(); } if (tmpFile != null) { if (!tmpFile.delete()) { LOG.warn("Failed to delete tmp file: " + tmpFile); } } } }