/* * Copyright 2007 Sun Microsystems, Inc. * * This file is part of jVoiceBridge. * * jVoiceBridge is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation and distributed hereunder * to you. * * jVoiceBridge 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 this program. If not, see <http://www.gnu.org/licenses/>. * * Sun designates this particular file as subject to the "Classpath" * exception as provided by Sun in the License file that accompanied this * code. */ package com.sun.voip; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.text.ParseException; import java.util.LinkedList; /** * Write audio data to a file */ public class Recorder extends Thread { private BufferedOutputStream bo; private FileOutputStream fo; private static final int BUFFER_SIZE = 16 * 1024; private static String defaultRecordDirectory = "."; private String recordPath; private boolean recordRtp; private static String fileSeparator = System.getProperty("file.separator"); private boolean done; public Recorder(String recordPath, String recordingType, MediaInfo mediaInfo) throws IOException { this(null, recordPath, recordingType, mediaInfo); } public Recorder(String recordDirectory, String recordPath, String recordingType, MediaInfo mediaInfo) throws IOException { this.recordPath = getAbsolutePath(recordDirectory, recordPath); if (recordingType.equalsIgnoreCase("Rtp")) { recordRtp = true; } else if (recordingType.equalsIgnoreCase("Au") == false) { throw new IOException("Invalid recording type " + recordingType); } openFile(mediaInfo); start(); } public static String getAbsolutePath(String recordDirectory, String recordPath) throws IOException { String osName = System.getProperty("os.name"); if (osName.indexOf("Windows") >= 0) { if (recordPath.substring(0,1).equals(fileSeparator) == true || recordPath.charAt(1) == ':') { return recordPath; } } else { if (recordPath.substring(0,1).equals(fileSeparator) == true) { return recordPath; } } /* * Not absolute */ if (recordDirectory == null) { recordDirectory = System.getProperty("com.sun.voip.server.Bridge.recordDirectory", defaultRecordDirectory); } String path = recordDirectory + fileSeparator + recordPath; try { checkPermission(path, false); } catch (ParseException e) { throw new IOException(e.getMessage()); } return recordDirectory + fileSeparator + recordPath; } private void openFile(MediaInfo mediaInfo) throws IOException { File recordFile = new File(recordPath); try { synchronized(this) { if (recordFile.exists()) { recordFile.delete(); } recordFile.createNewFile(); fo = new FileOutputStream(recordFile); bo = new BufferedOutputStream(fo, BUFFER_SIZE); if (recordRtp == false) { writeAuHeader(mediaInfo); } else { /* * Write RTP header */ byte[] buf = new byte[16]; buf[0] = (byte) 0x52; // R buf[1] = (byte) 0x54; // T buf[2] = (byte) 0x50; // P bo.write(buf, 0, buf.length); } } } catch (IOException e) { fo = null; bo = null; Logger.error("can't create buffered output stream for " + recordPath + " " + e.getMessage()); throw new IOException( "can't create buffered output stream for " + recordPath + " " + e.getMessage()); } Logger.println("Recording to " + recordFile.getAbsolutePath() + " recording type is " + (recordRtp ? "RTP" : "Audio")); } public String getRecordPath() { return recordPath; } public static void checkPermission(String recordPath) throws ParseException { checkPermission(recordPath, false); } public static void checkPermission(String recordPath, boolean isDirectory) throws ParseException { if (isDirectory) { File file = new File(recordPath); if (file.exists() == false) { throw new ParseException( "Non-existent directory: " + recordPath, 0); } if (file.isDirectory() == false) { throw new ParseException("Not a directory: " + recordPath, 0); } if (file.canWrite() == false) { throw new ParseException("Permission denied. Can't write " + recordPath, 0); } return; } File file = new File(recordPath); try { if (file.exists()) { if (file.isDirectory()) { throw new ParseException("Not a regular file: " + recordPath + ".", 0); } } /* * Try to create a file in the directory */ String directory = defaultRecordDirectory; int i = recordPath.lastIndexOf(fileSeparator); if (i > 0) { directory = recordPath.substring(0, i); } file = File.createTempFile("Record", "tmp", new File(directory)); file.delete(); } catch (IOException e) { throw new ParseException("Unable to create file " + recordPath + ". " + e.getMessage(), 0); } } private byte[] auHeader; private void writeAuHeader(MediaInfo mediaInfo) throws IOException { /* * write a .au header to the file * * magic: 4 bytes ".snd" * hdrsize: 4 bytes * datasize: 4 bytes 0 * encoding: 4 bytes 1 for ulaw, 3 for linear * sampleRate: 4 bytes * channels: 4 bytes */ auHeader = new byte[24]; auHeader[0] = '.'; auHeader[1] = 's'; auHeader[2] = 'n'; auHeader[3] = 'd'; auHeader[7] = 24; int encoding = mediaInfo.getEncoding(); if (encoding == RtpPacket.PCMU_ENCODING) { auHeader[15] = 1; } else { auHeader[15] = 3; } int sampleRate = mediaInfo.getSampleRate(); auHeader[16] = (byte)((sampleRate >> 24) & 0xff); auHeader[17] = (byte)((sampleRate >> 16) & 0xff); auHeader[18] = (byte)((sampleRate >> 8) & 0xff); auHeader[19] = (byte)(sampleRate & 0xff); int channels = mediaInfo.getChannels(); auHeader[20] = (byte)((channels >> 24) & 0xff); auHeader[21] = (byte)((channels >> 16) & 0xff); auHeader[22] = (byte)((channels >> 8) & 0xff); auHeader[23] = (byte)(channels & 0xff); bo.write(auHeader, 0, auHeader.length); } public void done() { if (done) { return; } done = true; synchronized(dataToWrite) { dataToWrite.notifyAll(); } } /* * Write data to a file */ private LinkedList dataToWrite = new LinkedList(); class DataToWrite { public byte[] data; public int offset; public int length; public DataToWrite(byte[] data, int offset, int length) { /* * We have to copy the data, otherwise caller could * overwrite it. */ this.data = new byte[length]; this.offset = offset; this.length = length; System.arraycopy(data, offset, this.data, 0, length); } } private long lastWriteTime; public void writePacket(byte[] data, int offset, int dataLength) throws IOException { if (recordRtp) { byte[] buf = new byte[dataLength + 4]; int timeChange; long now = System.currentTimeMillis(); if (lastWriteTime == 0) { timeChange = 0; } else { timeChange = (int) (now - lastWriteTime); } lastWriteTime = now; buf[0] = (byte) ((buf.length >> 8) & 0xff); buf[1] = (byte) (buf.length & 0xff); buf[2] = (byte) ((timeChange >> 8) & 0xff); buf[3] = (byte) (timeChange & 0xff); System.arraycopy(data, offset, buf, 4, dataLength); write(buf, 0, buf.length); } else { write(data, offset, dataLength); } } public void write(int[] data, int offset, int length) throws IOException { byte[] byteData = new byte[length * 2]; for (int i = 0; i < length; i++) { byteData[(2 * i)] = (byte) ((data[i + offset] >> 8) & 0xff); byteData[(2 * i) + 1] = (byte) (data[i + offset] & 0xff); } write(byteData, 0, byteData.length); } public void write(byte[] data, int offset, int length) throws IOException { if (done) { return; } synchronized(dataToWrite) { dataToWrite.add(new DataToWrite(data, offset, length)); dataToWrite.notifyAll(); } } private int dataSize; public void run() { long lastWriteTime = 0; while (true) { synchronized(dataToWrite) { if (done) { break; } if (dataToWrite.size() == 0) { try { dataToWrite.wait(); } catch (InterruptedException e) { } if (done) { break; } } while (dataToWrite.size() > 0) { writeData((DataToWrite) dataToWrite.remove(0)); } continue; } } /* * Flush out remaining data */ synchronized (dataToWrite) { while (dataToWrite.size() > 0) { writeData((DataToWrite) dataToWrite.remove(0)); } } writeDataSize(); } private void writeData(DataToWrite d) { try { synchronized(this) { bo.write(d.data, 0, d.length); dataSize += d.length; } } catch (IOException e) { Logger.println("Can't record to " + recordPath); done(); } } private void writeDataSize() { try { synchronized(this) { if (bo != null) { bo.flush(); bo.close(); fo.flush(); fo.close(); } if (auHeader != null) { /* * Now write the data size in the auHeader */ auHeader[8] = (byte) ((dataSize >> 24) & 0xff); auHeader[9] = (byte) ((dataSize >> 16) & 0xff); auHeader[10] = (byte) ((dataSize >> 8) & 0xff); auHeader[11] = (byte) (dataSize & 0xff); try { RandomAccessFile raf = new RandomAccessFile( recordPath, "rw"); raf.write(auHeader); raf.close(); } catch (FileNotFoundException e) { Logger.println( "Unable to write data size to recording " + recordPath + " " + e.getMessage()); } } } } catch (IOException e) { Logger.println("Exception closing recording " + recordPath + " " + e.getMessage()); } } public static void setDefaultRecordingDirectory( String defaultRecordDirectory) throws ParseException { checkPermission(defaultRecordDirectory, true); Recorder.defaultRecordDirectory = defaultRecordDirectory; Logger.println("Default recording directory set to " + defaultRecordDirectory); } public static String getRecordingDirectory() { return defaultRecordDirectory; } }