/* * @(#)Handler.java 1.26 02/08/21 * * Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved. */ package com.sun.media.datasink.file; import java.lang.reflect.Method; import java.lang.reflect.Constructor; import java.io.IOException; import java.io.File; import java.io.RandomAccessFile; import java.io.FileDescriptor; import javax.media.DataSink; import javax.media.Format; import javax.media.MediaLocator; import javax.media.Buffer; import javax.media.Control; import javax.media.protocol.Controls; import javax.media.protocol.SourceTransferHandler; import javax.media.datasink.DataSinkListener; import javax.media.datasink.EndOfStreamEvent; import javax.media.datasink.DataSinkErrorEvent; import javax.media.protocol.PushDataSource; import javax.media.protocol.PushSourceStream; import javax.media.protocol.PullDataSource; import javax.media.protocol.PullSourceStream; import javax.media.protocol.DataSource; import javax.media.protocol.SourceStream; import javax.media.protocol.Seekable; import javax.media.protocol.FileTypeDescriptor; import javax.media.IncompatibleSourceException; import com.sun.media.util.*; import com.sun.media.Syncable; import com.sun.media.JMFSecurity; import com.sun.media.JDK12Security; import com.sun.media.JMFSecurityManager; import com.sun.media.datasink.BasicDataSink; import com.sun.media.datasink.RandomAccess; import com.ms.security.PermissionID; import com.ms.security.PolicyEngine; import java.security.*; public class Handler extends BasicDataSink implements SourceTransferHandler, Seekable, Runnable, RandomAccess, Syncable { final private static boolean DEBUG = false; final protected static int NOT_INITIALIZED = 0; final protected static int OPENED = 1; final protected static int STARTED = 2; //final protected static int STOPPED = 3; final protected static int CLOSED = 3; protected int state = NOT_INITIALIZED; protected DataSource source; protected SourceStream [] streams; protected SourceStream stream; protected boolean push; protected boolean errorEncountered = false; protected String errorReason = null; protected Control [] controls; protected File file; protected File tempFile = null; protected RandomAccessFile raFile = null; protected RandomAccessFile qtStrRaFile = null; protected boolean fileClosed = false; protected FileDescriptor fileDescriptor = null; protected MediaLocator locator = null; protected String contentType = null; protected int fileSize = 0; // Good for over 2 Gigabytes protected int filePointer = 0; protected int bytesWritten = 0; protected static final int BUFFER_LEN = 128 * 1024; protected boolean syncEnabled = false; protected byte [] buffer1 = new byte[BUFFER_LEN]; protected byte [] buffer2 = new byte[BUFFER_LEN]; protected boolean buffer1Pending = false; protected long buffer1PendingLocation = -1; protected int buffer1Length; protected boolean buffer2Pending = false; protected long buffer2PendingLocation = -1; protected int buffer2Length; protected long nextLocation = 0; protected Thread writeThread = null; private Integer bufferLock = new Integer(0); private boolean receivedEOS = false; private static JMFSecurity jmfSecurity = null; private static boolean securityPrivelege=false; private Method m[] = new Method[1]; private Class cl[] = new Class[1]; private Object args[][] = new Object[1][0]; public int WRITE_CHUNK_SIZE = 16384; private boolean streamingEnabled = false; private boolean errorCreatingStreamingFile = false; static { try { jmfSecurity = JMFSecurityManager.getJMFSecurity(); securityPrivelege = true; } catch (SecurityException e) { } } public void setSource(DataSource ds) throws IncompatibleSourceException { if (!(ds instanceof PushDataSource) && !(ds instanceof PullDataSource)) { throw new IncompatibleSourceException("Incompatible datasource"); } source = ds; if (source instanceof PushDataSource) { push = true; try { ((PushDataSource) source).connect(); } catch (IOException ioe) { } streams = ((PushDataSource) source).getStreams(); } else { push = false; try { ((PullDataSource) source).connect(); } catch (IOException ioe) { } streams = ((PullDataSource) source).getStreams(); } if (streams == null || streams.length != 1) throw new IncompatibleSourceException("DataSource should have 1 stream"); stream = streams[0]; contentType = source.getContentType(); if (push) ((PushSourceStream)stream).setTransferHandler(this); } /** * Set the output <code>MediaLocator</code>. * This method should only be called once; an error is thrown if * the locator has already been set. * @param output <code>MediaLocator</code> that describes where * the output goes. */ public void setOutputLocator(MediaLocator output) { locator = output; } public void setEnabled(boolean b) { streamingEnabled = b; } public void setSyncEnabled() { syncEnabled = true; } // write chunk // Call with -1, -1 when done writing chunks to streamable file // Call with -1, (num > 0) to seek to num-1 and write 1 byte public boolean write(long inOffset, int numBytes) { try { if ( (inOffset >= 0) && (numBytes > 0 ) ) { int remaining = numBytes; int bytesToRead; raFile.seek(inOffset); while (remaining > 0) { bytesToRead = (remaining > BUFFER_LEN) ? BUFFER_LEN : remaining; raFile.read(buffer1, 0, bytesToRead); //$$ CAST qtStrRaFile.write(buffer1, 0, bytesToRead); //$$ CAST remaining -= bytesToRead; } } else if ( (inOffset < 0) && (numBytes > 0) ) { qtStrRaFile.seek(0); qtStrRaFile.seek(numBytes-1); qtStrRaFile.writeByte(0); qtStrRaFile.seek(0); } else { sendEndofStreamEvent(); } } catch (Exception e) { errorCreatingStreamingFile = true; System.err.println("Exception when creating streamable version of media file: " + e.getMessage()); return false; } return true; } public void open() throws IOException, SecurityException { try { if ( state == NOT_INITIALIZED ) { if (locator != null) { String pathName = locator.getRemainder(); // getFileName(locator); // Strip off excess /'s while (pathName.charAt(0) == '/' && (pathName.charAt(1) == '/' || pathName.charAt(2) == ':')) { pathName = pathName.substring(1); } String fileSeparator = System.getProperty("file.separator"); if (fileSeparator.equals("\\")) { pathName = pathName.replace('/', '\\'); } com.sun.media.JMFSecurityManager.checkFileSave(); if ( securityPrivelege && (jmfSecurity != null) ) { String permission = null; try { if (jmfSecurity.getName().startsWith("jmf-security")) { permission = "read file"; jmfSecurity.requestPermission(m, cl, args, JMFSecurity.READ_FILE); m[0].invoke(cl[0], args[0]); permission = "write file"; jmfSecurity.requestPermission(m, cl, args, JMFSecurity.WRITE_FILE); m[0].invoke(cl[0], args[0]); } else if (jmfSecurity.getName().startsWith("internet")) { PolicyEngine.checkPermission(PermissionID.FILEIO); PolicyEngine.assertPermission(PermissionID.FILEIO); } } catch (Exception e) { securityPrivelege = false; if (push) ((PushSourceStream)stream).setTransferHandler(null); throw new SecurityException(e.getMessage()); } catch (Error e) { securityPrivelege = false; if (push) ((PushSourceStream)stream).setTransferHandler(null); throw new SecurityException(e.getMessage()); } } if (!securityPrivelege) { if (push) ((PushSourceStream)stream).setTransferHandler(null); throw new IOException("Datasink: Unable to get security privileges for file writing"); } // In jdk1.2, RandomAccessFile has a useful method called // setLength() which can be used to truncate an existing // file. But this won't work on jdk1.1. So we have to // delete a file if it exists // On Windows, you cannot delete a file if some process // is using it. file = new File(pathName); if (file.exists()) { if (!deleteFile(file)) { System.err.println("datasink open: Existing file " + pathName + " cannot be deleted. Check if " + "some other process is using " + " this file"); if (push) ((PushSourceStream)stream).setTransferHandler(null); throw new IOException("Existing file " + pathName + " cannot be deleted"); } } String parent = file.getParent(); if (parent != null) { new File(parent).mkdirs(); } try { if (!streamingEnabled) { raFile = new RandomAccessFile(file, "rw"); fileDescriptor = raFile.getFD(); } else { String fileqt; int index; if ( (index = pathName.lastIndexOf(".")) > 0 ) { // TODO: maybe $$$ add random string fileqt = pathName.substring(0, index) + ".nonstreamable" + pathName.substring(index, pathName.length()); } else { // TODO: maybe $$$ add random string // TODO: Don't assume .mov if the file doesn't have // and extension. Try to use content type if possible // to guess the extension. // However, extensions will be there as JMF provides it // even if user doesn't. fileqt = file + ".nonstreamable.mov"; } tempFile = new File(fileqt); raFile = new RandomAccessFile(tempFile, "rw"); fileDescriptor = raFile.getFD(); qtStrRaFile = new RandomAccessFile(file, "rw"); } } catch (IOException e) { // Catch the exception for debugging purpose and // throw it again System.err.println("datasink open: IOException when creating RandomAccessFile " + pathName + " : " + e); if (push) ((PushSourceStream)stream).setTransferHandler(null); throw e; } setState(OPENED); } } } finally { if ( (state == NOT_INITIALIZED) && (stream != null) ) { ((PushSourceStream)stream).setTransferHandler(null); } } } public MediaLocator getOutputLocator() { return locator; } public void start() throws IOException { if (state == OPENED) { if (source != null) source.start(); if (writeThread == null) { writeThread = new Thread(this); writeThread.start(); } setState(STARTED); } } /** * Stop the data-transfer. * If the source has not been connected and started, * <CODE>stop</CODE> does nothing. */ public void stop() throws IOException { if (state == STARTED) { if (source != null) source.stop(); setState(OPENED); } } protected void setState(int state) { synchronized(this) { this.state = state; } } public void close() { close(null); // No Error; } protected final void close(String reason) { synchronized(this) { if ( state == CLOSED ) return; setState(CLOSED); } if (push) { for (int i = 0; i < streams.length; i++) { ((PushSourceStream)streams[i]).setTransferHandler(null); } } if (reason != null) { errorEncountered = true; sendDataSinkErrorEvent(reason); // Wake up the write thread synchronized (bufferLock) { bufferLock.notifyAll(); } } // Wait for all buffers to be written /* synchronized (bufferLock) { while (reason == null && (buffer2Pending || buffer1Pending)) { try { bufferLock.wait(250); } catch (InterruptedException ie) { } } } */ try { source.stop(); } catch (IOException e) { System.err.println("IOException when stopping source " + e); } try { if (raFile != null) { raFile.close(); } if (streamingEnabled) { if (qtStrRaFile != null) { qtStrRaFile.close(); } } // Disconnect the data source if (source != null) source.disconnect(); //////////////////////////////////////////////////////////// if (streamingEnabled && (tempFile != null) ) { // Delete the temp file if no errors creating streamable file. // If errors creating streamable file, delete streamable file. if (!errorCreatingStreamingFile) { boolean status = deleteFile(tempFile); } else { boolean status = deleteFile(file); } } // fileClosed = true; // sendEndofStreamEvent(); } catch (IOException e) { System.out.println("close: " + e); } raFile = null; // Should be done after setting the state to CLOSED qtStrRaFile = null; removeAllListeners(); } public String getContentType() { return contentType; } public Object [] getControls() { if (controls == null) { controls = new Control[0]; } return controls; } public Object getControl(String controlName) { return null; } // TODO : Handle pull data source public synchronized void transferData(PushSourceStream pss) { int totalRead = 0; int spaceAvailable = BUFFER_LEN; int bytesRead = 0; if (errorEncountered) return; if (buffer1Pending) { synchronized (bufferLock) { while (buffer1Pending) { if (DEBUG) System.err.println("Waiting for free buffer"); try { bufferLock.wait(); } catch (InterruptedException ie) { } } } if (DEBUG) System.err.println("Got free buffer"); } // System.err.println("In transferData()"); while (spaceAvailable > 0) { try { bytesRead = pss.read(buffer1, totalRead, spaceAvailable); //System.err.println("bytesRead = " + bytesRead); if (bytesRead > 16 * 1024 && WRITE_CHUNK_SIZE < 32 * 1024) { if ( bytesRead > 64 * 1024 && WRITE_CHUNK_SIZE < 128 * 1024 ) WRITE_CHUNK_SIZE = 128 * 1024; else if ( bytesRead > 32 * 1024 && WRITE_CHUNK_SIZE < 64 * 1024 ) WRITE_CHUNK_SIZE = 64 * 1024; else if ( WRITE_CHUNK_SIZE < 32 * 1024 ) WRITE_CHUNK_SIZE = 32 * 1024; //System.err.println("Increasing buffer to " + WRITE_CHUNK_SIZE); } } catch (IOException ioe) { // What to do here? } if (bytesRead <= 0) { break; } totalRead += bytesRead; spaceAvailable -= bytesRead; } if (totalRead > 0) { synchronized (bufferLock) { buffer1Pending = true; buffer1PendingLocation = nextLocation; buffer1Length = totalRead; nextLocation = -1; // assume next write is contiguous unless seeked // Notify availability to write thread if (DEBUG) System.err.println("Notifying consumer"); bufferLock.notifyAll(); } } // Send EOS if necessary if (bytesRead == -1) { if (DEBUG) System.err.println("Got EOS"); receivedEOS = true; // Wait until file is closed. This makes the Processor's close // call to force the data sink to close the file, just in case // the user doesn't remember to close the datasink before exiting. while (!fileClosed && !errorEncountered && !(state == CLOSED)) { try { Thread.sleep(50); } catch (InterruptedException ie) { } } } } // Asynchronous write thread public void run() { while (!(state == CLOSED || errorEncountered)) { synchronized (bufferLock) { // Wait for some data or error while (!buffer1Pending && !buffer2Pending && !errorEncountered && state != CLOSED && !receivedEOS) { if (DEBUG) System.err.println("Waiting for filled buffer"); try { bufferLock.wait(500); } catch (InterruptedException ie) { } if (DEBUG) System.err.println("Consumer notified"); } } // Something's pending if (buffer2Pending) { if (DEBUG) System.err.println("Writing Buffer2"); // write that first write(buffer2, buffer2PendingLocation, buffer2Length); if (DEBUG) System.err.println("Done writing Buffer2"); buffer2Pending = false; } synchronized (bufferLock) { if (buffer1Pending) { byte [] tempBuffer = buffer2; buffer2 = buffer1; buffer2Pending = true; buffer2PendingLocation = buffer1PendingLocation; buffer2Length = buffer1Length; buffer1Pending = false; buffer1 = tempBuffer; if (DEBUG) System.err.println("Notifying producer"); bufferLock.notifyAll(); } else { if (receivedEOS) break; } } } if (receivedEOS) { if (DEBUG) System.err.println("Sending EOS: streamingEnabled is " + streamingEnabled); // Close the file and flag it if (raFile != null) { if (!streamingEnabled) { try { raFile.close(); } catch (IOException ioe) { } raFile = null; } fileClosed = true; } if (!streamingEnabled) { sendEndofStreamEvent(); } } if (errorEncountered && state != CLOSED) { close(errorReason); } } public synchronized long seek(long where) { nextLocation = where; return where; } long lastSyncTime = -1; private void write(byte [] buffer, long location, int length) { int offset, toWrite; try { if (location != -1) doSeek(location); offset = 0; while (length > 0) { toWrite = WRITE_CHUNK_SIZE; if (length < toWrite) toWrite = length; raFile.write(buffer, offset, toWrite); bytesWritten += toWrite; // Sync/Flush after a few write so that the // file writing is smooth. Improves capture smoothness /* if (fileDescriptor != null) { // Sync'ing the file system at every 1 sec interval. if (lastSyncTime < 0) lastSyncTime = System.currentTimeMillis(); else { long ts = System.currentTimeMillis(); if (ts - lastSyncTime > 1000L) { fileDescriptor.sync(); //System.err.println("sync: byte written: " + bytesWritten); bytesWritten = 0; lastSyncTime = ts; } } } */ if ( fileDescriptor != null && syncEnabled && bytesWritten >= WRITE_CHUNK_SIZE) { bytesWritten -= WRITE_CHUNK_SIZE; fileDescriptor.sync(); } filePointer += toWrite; length -= toWrite; offset += toWrite; if (filePointer > fileSize) fileSize = filePointer; Thread.yield(); } } catch (IOException ioe) { errorEncountered = true; errorReason = ioe.toString(); } } public long doSeek(long where) { if (raFile != null) { try { raFile.seek(where); filePointer = (int) where; return where; } catch (IOException ioe) { close("Error in seek: " + ioe); } } return -1; } public long tell() { return nextLocation; } public long doTell() { if (raFile != null) { try { return raFile.getFilePointer(); } catch (IOException ioe) { close("Error in tell: " + ioe); } } return -1; } public boolean isRandomAccess() { return true; } private boolean deleteFile(File file) { boolean fileDeleted=false; try { if ( /*securityPrivelege &&*/ (jmfSecurity != null) ) { try { if (jmfSecurity.getName().startsWith("jmf-security")) { jmfSecurity.requestPermission(m, cl, args, JMFSecurity.DELETE_FILE); m[0].invoke(cl[0], args[0]); } else if (jmfSecurity.getName().startsWith("internet")) { PolicyEngine.checkPermission(PermissionID.FILEIO); PolicyEngine.assertPermission(PermissionID.FILEIO); } } catch (Exception e) { if (JMFSecurityManager.DEBUG) { System.err.println("Unable to get DELETE_FILE " + " privilege " + e); } securityPrivelege = false; // TODO: Do the right thing if permissions cannot be obtained. // User should be notified via an event, if applicable } } if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) { Constructor cons = jdk12DeleteFileAction.cons; Boolean success; success = (Boolean) jdk12.doPrivM.invoke( jdk12.ac, new Object[] { cons.newInstance( new Object[] { file }) }); fileDeleted = success.booleanValue(); } else { fileDeleted = file.delete(); } } catch (Throwable e) { } return fileDeleted; } }