Search code examples
javajarjmf

How to load media resources from the classpath in JMF


I have a Java application that I want to turn into an executable jar. I am using JMF in this application, and I can't seem to get the sound files working right...

I create the jar using

jar cvfm jarname.jar manifest.txt *.class *.gif *.wav

So, all the sound files get put inside the jar, and in the code, I am creating the Players using

Player player = Manager.createPlayer(ClassName.class.getResource("song1.wav"));

The jar is on my desktop, and when I attempt to run it, this exception occurs:

javax.media.NoPlayerException: Cannot find a Player for :jar:file:/C:/Users/Pojo/
Desktop/jarname.jar!/song1.wav

...It's not getting IOExceptions, so it seems to at least be finding the file itself all right.

Also, before I used the getResource, I used to have it like this:

Player player = Manager.createPlayer(new File("song1.wav").toURL());

and it was playing fine, so I know nothing is wrong with the sound file itself.

The reason I am trying to switch to this method instead of the File method is so that the sound files can be packaged inside the jar itself and not have to be its siblings in a directory.


Solution

  • New solution:

    First, a custom DataSource class that returns a SourceStream that implements Seekable is needed:

    package com.ziesemer.test;
    
    import java.io.Closeable;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.JarURLConnection;
    import java.net.URL;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    
    import javax.media.Duration;
    import javax.media.MediaLocator;
    import javax.media.Time;
    import javax.media.protocol.ContentDescriptor;
    import javax.media.protocol.PullDataSource;
    import javax.media.protocol.PullSourceStream;
    import javax.media.protocol.Seekable;
    
    /**
     * @author Mark A. Ziesemer
     *  <a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
     */
    public class JarDataSource extends PullDataSource{
    
        protected JarURLConnection conn;
        protected ContentDescriptor contentType;
        protected JarPullSourceStream[] sources;
        protected boolean connected;
    
        public JarDataSource(URL url) throws IOException{
            setLocator(new MediaLocator(url));
            connected = false;
        }
    
        @Override
        public PullSourceStream[] getStreams(){
            return sources;
        }
    
        @Override
        public void connect() throws IOException{
            conn = (JarURLConnection)getLocator().getURL().openConnection();
            conn.connect();
            connected = true;
    
            JarFile jf = conn.getJarFile();
            JarEntry je = jf.getJarEntry(conn.getEntryName());
    
            String mimeType = conn.getContentType();
            if(mimeType == null){
                mimeType = ContentDescriptor.CONTENT_UNKNOWN;
            }
            contentType = new ContentDescriptor(ContentDescriptor.mimeTypeToPackageName(mimeType));
    
            sources = new JarPullSourceStream[1];
            sources[0] = new JarPullSourceStream(jf, je, contentType);
        }
    
        @Override
        public String getContentType(){
            return contentType.getContentType();
        }
    
        @Override
        public void disconnect(){
            if(connected){
                try{
                    sources[0].close();
                }catch(IOException e){
                    e.printStackTrace();
                }
                connected = false;
            }
        }
    
        @Override
        public void start() throws IOException{
            // Nothing to do.
        }
    
        @Override
        public void stop() throws IOException{
            // Nothing to do.
        }
    
        @Override
        public Time getDuration(){
            return Duration.DURATION_UNKNOWN;
        }
    
        @Override
        public Object[] getControls(){
            return new Object[0];
        }
    
        @Override
        public Object getControl(String controlName){
            return null;
        }
    
        protected class JarPullSourceStream implements PullSourceStream, Seekable, Closeable{
    
            protected final JarFile jarFile;
            protected final JarEntry jarEntry;
            protected final ContentDescriptor type;
    
            protected InputStream stream;
            protected long position;
    
            public JarPullSourceStream(JarFile jarFile, JarEntry jarEntry, ContentDescriptor type) throws IOException{
                this.jarFile = jarFile;
                this.jarEntry = jarEntry;
                this.type = type;
                this.stream = jarFile.getInputStream(jarEntry);
            }
    
            @Override
            public ContentDescriptor getContentDescriptor(){
                return type;
            }
    
            @Override
            public long getContentLength(){
                return jarEntry.getSize();
            }
    
            @Override
            public boolean endOfStream(){
                return position < getContentLength();
            }
    
            @Override
            public Object[] getControls(){
                return new Object[0];
            }
    
            @Override
            public Object getControl(String controlType){
                return null;
            }
    
            @Override
            public boolean willReadBlock(){
                if(endOfStream()){
                    return true;
                }
                try{
                    return stream.available() == 0;
                }catch(IOException e){
                    return true;
                }
            }
    
            @Override
            public int read(byte[] buffer, int offset, int length) throws IOException{
                int read = stream.read(buffer, offset, length);
                position += read;
                return read;
            }
    
            @Override
            public long seek(long where){
                try{
                    if(where < position){
                        stream.close();
                        stream = jarFile.getInputStream(jarEntry);
                        position = 0;
                    }
                    long skip = where - position;
                    while(skip > 0){
                        long skipped = stream.skip(skip);
                        skip -= skipped;
                        position += skipped;
                    }
                }catch(IOException ioe){
                    // Made a best effort.
                    ioe.printStackTrace();
                }
                return position;
            }
    
            @Override
            public long tell(){
                return position;
            }
    
            @Override
            public boolean isRandomAccess(){
                return true;
            }
    
            @Override
            public void close() throws IOException{
                try{
                    stream.close();
                }finally{
                    jarFile.close();
                }
            }
    
        }
    
    }
    

    Then, the above custom data source is used to create a player, and a ControllerListener is added to cause the player to loop:

    package com.ziesemer.test;
    
    import java.net.URL;
    
    import javax.media.ControllerEvent;
    import javax.media.ControllerListener;
    import javax.media.EndOfMediaEvent;
    import javax.media.Manager;
    import javax.media.Player;
    import javax.media.Time;
    
    /**
     * @author Mark A. Ziesemer
     *  <a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
     */
    public class JmfTest{
        public static void main(String[] args) throws Exception{
            URL url = JmfTest.class.getResource("Test.wav");
            JarDataSource jds = new JarDataSource(url);
            jds.connect();
            final Player player = Manager.createPlayer(jds);
    
            player.addControllerListener(new ControllerListener(){
                @Override
                public void controllerUpdate(ControllerEvent ce){
                    if(ce instanceof EndOfMediaEvent){
                        player.setMediaTime(new Time(0));
                        player.start();
                    }
                }
            });
            player.start();
        }
    }
    

    Note that without the custom data source, JMF tries repeatedly to seek back to the beginning - but fails, and eventually gives up. This can be seen from debugging the same ControllerListener, which will receive a several events for each attempt.

    Or, using the MediaPlayer approach to loop (that you mentioned on my previous answer):

    package com.ziesemer.test;
    
    import java.net.URL;
    
    import javax.media.Manager;
    import javax.media.Player;
    import javax.media.bean.playerbean.MediaPlayer;
    
    /**
     * @author Mark A. Ziesemer
     *  <a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
     */
    public class JmfTest{
        public static void main(String[] args) throws Exception{
            URL url = JmfTest.class.getResource("Test.wav");
            JarDataSource jds = new JarDataSource(url);
            jds.connect();
            final Player player = Manager.createPlayer(jds);
    
            MediaPlayer mp = new MediaPlayer();
            mp.setPlayer(player);
            mp.setPlaybackLoop(true);
            mp.start();
        }
    }
    

    Again, I would not consider this production-ready code (could use some more Javadocs and logging, etc.), but it is tested and working (Java 1.6), and should meet your needs nicely.

    Merry Christmas, and happy holidays!