org.red5.io.mp3.impl.MP3Reader.java Source code

Java tutorial

Introduction

Here is the source code for org.red5.io.mp3.impl.MP3Reader.java

Source

package org.red5.io.mp3.impl;

/*
 * RED5 Open Source Flash Server - http://www.osflash.org/red5
 * 
 * Copyright (c) 2006-2007 by respective authors (see below). All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.mina.common.ByteBuffer;
import org.red5.io.IKeyFrameMetaCache;
import org.red5.io.IStreamableFile;
import org.red5.io.ITag;
import org.red5.io.ITagReader;
import org.red5.io.IoConstants;
import org.red5.io.amf.Output;
import org.red5.io.flv.IKeyFrameDataAnalyzer;
import org.red5.io.flv.impl.Tag;
import org.red5.io.object.Serializer;

/**
 * Read MP3 files
 */
public class MP3Reader implements ITagReader, IKeyFrameDataAnalyzer {
    /**
     * Logger
     */
    protected static Log log = LogFactory.getLog(MP3Reader.class.getName());

    /**
     * File
     */
    private File file;
    /**
     * File input stream
     */
    private FileInputStream fis;
    /**
     * File channel
     */
    private FileChannel channel;
    /**
     * Memory-mapped buffer for file content
     */
    private MappedByteBuffer mappedFile;
    /**
     * Source byte buffer
     */
    private ByteBuffer in;
    /**
     * Last read tag object
     */
    private ITag tag;
    /**
     * Previous tag size
     */
    private int prevSize;
    /**
     * Current time
     */
    private double currentTime;
    /**
     * Frame metadata
     */
    private KeyFrameMeta frameMeta;
    /**
     * Positions and time map
     */
    private HashMap<Integer, Double> posTimeMap;

    private int dataRate;
    /**
     * Whether first frame is read
     */
    private boolean firstFrame;
    /**
     * File metadata
     */
    private ITag fileMeta;
    /**
     * File duration
     */
    private long duration;
    /**
     * Frame cache
     */
    static private IKeyFrameMetaCache frameCache;

    MP3Reader() {
        // Only used by the bean startup code to initialize the frame cache
    }

    /**
     * Creates reader from file input stream
     * @param stream          File input stream source
     */
    public MP3Reader(File file) throws FileNotFoundException {
        this.file = file;
        fis = new FileInputStream(file);
        // Grab file channel and map it to memory-mapped byte buffer in read-only mode
        channel = fis.getChannel();
        try {
            mappedFile = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        } catch (IOException e) {
            log.error("MP3Reader :: MP3Reader ::>\n", e);
        }

        // Use Big Endian bytes order
        mappedFile.order(ByteOrder.BIG_ENDIAN);
        // Wrap mapped byte buffer to MINA buffer
        in = ByteBuffer.wrap(mappedFile);
        // Analyze keyframes data
        analyzeKeyFrames();
        firstFrame = true;

        // Process ID3v2 header if present
        processID3v2Header();

        // Create file metadata object
        fileMeta = createFileMeta();

        // MP3 header is length of 32 bits, that is, 4 bytes
        // Read further if there's still data
        if (in.remaining() > 4) {
            // Look to next frame
            searchNextFrame();
            // Set position
            int pos = in.position();
            // Read header...
            // Data in MP3 file goes header-data-header-data...header-data
            MP3Header header = readHeader();
            // Set position
            in.position(pos);
            // Check header
            if (header != null) {
                checkValidHeader(header);
            } else {
                throw new RuntimeException("No initial header found.");
            }
        }
    }

    /**
     * A MP3 stream never has video.
     * 
     * @return always returns <code>false</code>
     */
    public boolean hasVideo() {
        return false;
    }

    public void setFrameCache(IKeyFrameMetaCache frameCache) {
        MP3Reader.frameCache = frameCache;
    }

    /**
     * Check if the file can be played back with Flash. Supported sample rates are
      * 44KHz, 22KHz, 11KHz and 5.5KHz
      * 
     * @param header       Header to check
     */
    private void checkValidHeader(MP3Header header) {
        switch (header.getSampleRate()) {
        case 44100:
        case 22050:
        case 11025:
        case 5513:
            // Supported sample rate
            break;

        default:
            throw new RuntimeException("Unsupported sample rate: " + header.getSampleRate());
        }
    }

    /**
     * Creates file metadata object
     * @return         Tag
     */
    private ITag createFileMeta() {
        // Create tag for onMetaData event
        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.setAutoExpand(true);
        Output out = new Output(buf);
        out.writeString("onMetaData");
        Map<Object, Object> props = new HashMap<Object, Object>();
        props.put("duration", frameMeta.timestamps[frameMeta.timestamps.length - 1] / 1000.0);
        props.put("audiocodecid", IoConstants.FLAG_FORMAT_MP3);
        if (dataRate > 0) {
            props.put("audiodatarate", dataRate);
        }
        props.put("canSeekToEnd", true);
        out.writeMap(props, new Serializer());
        buf.flip();

        ITag result = new Tag(IoConstants.TYPE_METADATA, 0, buf.limit(), null, prevSize);
        result.setBody(buf);
        return result;
    }

    /** Search for next frame sync word. Sync word identifies valid frame. */
    public void searchNextFrame() {
        while (in.remaining() > 1) {
            int ch = in.get() & 0xff;
            if (ch != 0xff) {
                continue;
            }

            if ((in.get() & 0xe0) == 0xe0) {
                // Found it
                in.position(in.position() - 2);
                return;
            }
        }
    }

    /** {@inheritDoc} */
    public IStreamableFile getFile() {
        // TODO Auto-generated method stub
        return null;
    }

    /** {@inheritDoc} */
    public int getOffset() {
        // TODO Auto-generated method stub
        return 0;
    }

    /** {@inheritDoc} */
    public long getBytesRead() {
        return in.position();
    }

    /** {@inheritDoc} */
    public long getDuration() {
        return duration;
    }

    /** {@inheritDoc} */
    public boolean hasMoreTags() {
        MP3Header header = null;
        while (header == null && in.remaining() > 4) {
            try {
                header = new MP3Header(in.getInt());
            } catch (IOException e) {
                log.error("MP3Reader :: hasMoreTags ::>\n", e);
                break;
            } catch (Exception e) {
                searchNextFrame();
            }
        }

        if (header == null) {
            return false;
        }

        if (header.frameSize() == 0) {
            // TODO find better solution how to deal with broken files...
            // See APPSERVER-62 for details
            return false;
        }

        if (in.position() + header.frameSize() - 4 > in.limit()) {
            // Last frame is incomplete
            in.position(in.limit());
            return false;
        }

        in.position(in.position() - 4);
        return true;
    }

    private MP3Header readHeader() {
        MP3Header header = null;
        while (header == null && in.remaining() > 4) {
            try {
                header = new MP3Header(in.getInt());
            } catch (IOException e) {
                log.error("MP3Reader :: readTag ::>\n", e);
                break;
            } catch (Exception e) {
                searchNextFrame();
            }
        }
        return header;
    }

    /** {@inheritDoc} */
    public synchronized ITag readTag() {
        if (firstFrame) {
            // Return file metadata as first tag.
            firstFrame = false;
            return fileMeta;
        }

        MP3Header header = readHeader();
        if (header == null) {
            return null;
        }

        int frameSize = header.frameSize();
        if (frameSize == 0) {
            // TODO find better solution how to deal with broken files...
            // See APPSERVER-62 for details
            return null;
        }

        if (in.position() + frameSize - 4 > in.limit()) {
            // Last frame is incomplete
            in.position(in.limit());
            return null;
        }

        tag = new Tag(IoConstants.TYPE_AUDIO, (int) currentTime, frameSize + 1, null, prevSize);
        prevSize = frameSize + 1;
        currentTime += header.frameDuration();
        ByteBuffer body = ByteBuffer.allocate(tag.getBodySize());
        byte tagType = (IoConstants.FLAG_FORMAT_MP3 << 4) | (IoConstants.FLAG_SIZE_16_BIT << 1);
        switch (header.getSampleRate()) {
        case 44100:
            tagType |= IoConstants.FLAG_RATE_44_KHZ << 2;
            break;
        case 22050:
            tagType |= IoConstants.FLAG_RATE_22_KHZ << 2;
            break;
        case 11025:
            tagType |= IoConstants.FLAG_RATE_11_KHZ << 2;
            break;
        default:
            tagType |= IoConstants.FLAG_RATE_5_5_KHZ << 2;
        }
        tagType |= (header.isStereo() ? IoConstants.FLAG_TYPE_STEREO : IoConstants.FLAG_TYPE_MONO);
        body.put(tagType);
        final int limit = in.limit();
        body.putInt(header.getData());
        in.limit(in.position() + frameSize - 4);
        body.put(in);
        body.flip();
        in.limit(limit);

        tag.setBody(body);

        return tag;
    }

    /** {@inheritDoc} */
    public void close() {
        if (posTimeMap != null) {
            posTimeMap.clear();
        }
        mappedFile.clear();
        if (in != null) {
            in.release();
            in = null;
        }
        try {
            fis.close();
            channel.close();
        } catch (IOException e) {
            log.error("MP3Reader :: close ::>\n", e);
        }
    }

    /** {@inheritDoc} */
    public void decodeHeader() {
    }

    /** {@inheritDoc} */
    public void position(long pos) {
        if (pos == Long.MAX_VALUE) {
            // Seek at EOF
            in.position(in.limit());
            currentTime = duration;
            return;
        }
        in.position((int) pos);
        // Advance to next frame
        searchNextFrame();
        // Make sure we can resolve file positions to timestamps
        analyzeKeyFrames();
        Double time = posTimeMap.get(in.position());
        if (time != null) {
            currentTime = time;
        } else {
            // Unknown frame position - this should never happen
            currentTime = 0;
        }
    }

    /** {@inheritDoc} */
    public synchronized KeyFrameMeta analyzeKeyFrames() {
        if (frameMeta != null) {
            return frameMeta;
        }

        // check for cached frame informations
        if (frameCache != null) {
            frameMeta = frameCache.loadKeyFrameMeta(file);
            if (frameMeta != null && frameMeta.duration > 0) {
                // Frame data loaded, create other mappings
                duration = frameMeta.duration;
                frameMeta.audioOnly = true;
                posTimeMap = new HashMap<Integer, Double>();
                for (int i = 0; i < frameMeta.positions.length; i++) {
                    posTimeMap.put((int) frameMeta.positions[i], (double) frameMeta.timestamps[i]);
                }
                return frameMeta;
            }
        }

        List<Integer> positionList = new ArrayList<Integer>();
        List<Double> timestampList = new ArrayList<Double>();
        dataRate = 0;
        long rate = 0;
        int count = 0;
        int origPos = in.position();
        double time = 0;
        in.position(0);
        processID3v2Header();
        searchNextFrame();
        while (this.hasMoreTags()) {
            MP3Header header = readHeader();
            if (header == null) {
                // No more tags
                break;
            }

            if (header.frameSize() == 0) {
                // TODO find better solution how to deal with broken files...
                // See APPSERVER-62 for details
                break;
            }

            int pos = in.position() - 4;
            if (pos + header.frameSize() > in.limit()) {
                // Last frame is incomplete
                break;
            }

            positionList.add(pos);
            timestampList.add(time);
            rate += header.getBitRate() / 1000;
            time += header.frameDuration();
            in.position(pos + header.frameSize());
            count++;
        }
        // restore the pos
        in.position(origPos);

        duration = (long) time;
        dataRate = (int) (rate / count);
        posTimeMap = new HashMap<Integer, Double>();
        frameMeta = new KeyFrameMeta();
        frameMeta.duration = duration;
        frameMeta.positions = new long[positionList.size()];
        frameMeta.timestamps = new int[timestampList.size()];
        frameMeta.audioOnly = true;
        for (int i = 0; i < frameMeta.positions.length; i++) {
            frameMeta.positions[i] = positionList.get(i);
            frameMeta.timestamps[i] = timestampList.get(i).intValue();
            posTimeMap.put(positionList.get(i), timestampList.get(i));
        }
        if (frameCache != null)
            frameCache.saveKeyFrameMeta(file, frameMeta);

        return frameMeta;
    }

    private void processID3v2Header() {
        if (in.remaining() <= 10)
            // We need at least 10 bytes ID3v2 header + data
            return;

        int start = in.position();
        byte a, b, c;
        a = in.get();
        b = in.get();
        c = in.get();
        if (a != 'I' || b != 'D' || c != '3') {
            // No ID3v2 header
            in.position(start);
            return;
        }

        // Skip version and flags
        in.skip(3);
        int size = (in.get() & 0x7f) << 21 | (in.get() & 0x7f) << 14 | (in.get() & 0x7f) << 7 | (in.get() & 0x7f);
        // Skip ID3v2 header for now
        in.skip(size);
    }

}