package org.red5.io.mp3;
/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright (c) 2006-2010 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.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.mina.core.buffer.IoBuffer;
//import org.jaudiotagger.audio.AudioFileIO;
//import org.jaudiotagger.audio.mp3.MP3AudioHeader;
//import org.jaudiotagger.audio.mp3.MP3File;
//import org.jaudiotagger.tag.TagException;
//import org.jaudiotagger.tag.TagField;
//import org.jaudiotagger.tag.FieldKey;
//import org.jaudiotagger.tag.datatype.DataTypes;
//import org.jaudiotagger.tag.id3.AbstractID3v2Frame;
//import org.jaudiotagger.tag.id3.ID3v24Tag;
//import org.jaudiotagger.tag.id3.framebody.FrameBodyAPIC;
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.Tag;
import org.red5.io.object.Serializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Read MP3 files
*/
public class MP3Reader implements ITagReader, IKeyFrameDataAnalyzer {
/**
* Logger
*/
protected static Logger log = LoggerFactory.getLogger(MP3Reader.class);
/**
* 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 IoBuffer 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;
/**
* File duration
*/
private long duration;
/**
* Frame cache
*/
static private IKeyFrameMetaCache frameCache;
/**
* Holder for ID3 meta data
*/
private MetaData metaData;
/**
* Container for metadata and any other tags that should
* be sent prior to media data.
*/
private LinkedList<ITag> firstTags = new LinkedList<ITag>();
MP3Reader() {
// Only used by the bean startup code to initialize the frame cache
}
/**
* Creates reader from file input stream
* @param file file input
*
* @throws FileNotFoundException if not found
*/
public MP3Reader(File file) throws FileNotFoundException {
this.file = file;
// parse the id3 info
/*
try {
MP3File mp3file = (MP3File) AudioFileIO.read(file);
MP3AudioHeader audioHeader = (MP3AudioHeader) mp3file.getAudioHeader();
if (audioHeader != null) {
log.debug("Track length: {}", audioHeader.getTrackLength());
log.debug("Sample rate: {}", audioHeader.getSampleRateAsNumber());
log.debug("Channels: {}", audioHeader.getChannels());
log.debug("Variable bit rate: {}", audioHeader.isVariableBitRate());
log.debug("Track length (2): {}", audioHeader.getTrackLengthAsString());
log.debug("Mpeg version: {}", audioHeader.getMpegVersion());
log.debug("Mpeg layer: {}", audioHeader.getMpegLayer());
log.debug("Original: {}", audioHeader.isOriginal());
log.debug("Copyrighted: {}", audioHeader.isCopyrighted());
log.debug("Private: {}", audioHeader.isPrivate());
log.debug("Protected: {}", audioHeader.isProtected());
log.debug("Bitrate: {}", audioHeader.getBitRate());
log.debug("Encoding type: {}", audioHeader.getEncodingType());
log.debug("Encoder: {}", audioHeader.getEncoder());
}
ID3v24Tag idTag = mp3file.getID3v2TagAsv24();
if (idTag != null) {
// create meta data holder
metaData = new MetaData();
// metaData.setAlbum(idTag.getFirstAlbum());
// metaData.setArtist(idTag.getFirstArtist());
// metaData.setComment(idTag.getFirstComment());
// metaData.setGenre(idTag.getFirstGenre());
// metaData.setSongName(idTag.getFirstTitle());
// metaData.setTrack(idTag.getFirstTrack());
// metaData.setYear(idTag.getFirstYear());
//send album image if included
List<TagField> tagFieldList = mp3file.getTag().getFields(FieldKey.COVER_ART);
//fix for APPSERVER-310
if (tagFieldList == null || tagFieldList.isEmpty()) {
log.debug("No cover art was found");
} else {
TagField imageField = tagFieldList.get(0);
if (imageField instanceof AbstractID3v2Frame) {
FrameBodyAPIC imageFrameBody = (FrameBodyAPIC)((AbstractID3v2Frame)imageField).getBody();
if (!imageFrameBody.isImageUrl()) {
byte[] imageBuffer = (byte[]) imageFrameBody.getObjectValue(DataTypes.OBJ_PICTURE_DATA);
//set the cover image on the metadata
metaData.setCovr(imageBuffer);
// Create tag for onImageData event
IoBuffer buf = IoBuffer.allocate(imageBuffer.length);
buf.setAutoExpand(true);
Output out = new Output(buf);
out.writeString("onImageData");
Map<Object, Object> props = new HashMap<Object, Object>();
props.put("trackid", 1);
props.put("data", imageBuffer);
out.writeMap(props, new Serializer());
buf.flip();
//Ugh i hate flash sometimes!!
//Error #2095: flash.net.NetStream was unable to invoke callback onImageData.
ITag result = new Tag(IoConstants.TYPE_METADATA, 0, buf.limit(), null, 0);
result.setBody(buf);
//add to first frames
firstTags.add(result);
}
}
}
} else {
log.info("File did not contain ID3v2 data: {}", file.getName());
}
mp3file = null;
} catch (TagException e) {
log.error("MP3Reader (tag error) {}", e);
} catch (Exception e) {
log.error("MP3Reader {}", e);
}
*/
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 {}", e);
}
// Use Big Endian bytes order
mappedFile.order(ByteOrder.BIG_ENDIAN);
// Wrap mapped byte buffer to MINA buffer
in = IoBuffer.wrap(mappedFile);
// Analyze keyframes data
analyzeKeyFrames();
// Create file metadata object
firstTags.addFirst(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 48000:
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
IoBuffer buf = IoBuffer.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);
//set id3 meta data if it exists
if (metaData != null) {
props.put("artist", metaData.getArtist());
props.put("album", metaData.getAlbum());
props.put("songName", metaData.getSongName());
props.put("genre", metaData.getGenre());
props.put("year", metaData.getYear());
props.put("track", metaData.getTrack());
props.put("comment", metaData.getComment());
if (metaData.hasCoverImage()) {
Map<Object, Object> covr = new HashMap<Object, Object>(1);
covr.put("covr", new Object[]{metaData.getCovr()});
props.put("tags", covr);
}
//clear meta for gc
metaData = null;
}
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() {
return null;
}
/** {@inheritDoc} */
public int getOffset() {
return 0;
}
/** {@inheritDoc} */
public long getBytesRead() {
return in.position();
}
/** {@inheritDoc} */
public long getDuration() {
return duration;
}
/**
* Get the total readable bytes in a file or ByteBuffer.
*
* @return Total readable bytes
*/
public long getTotalBytes() {
return in.capacity();
}
/** {@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 (!firstTags.isEmpty()) {
// Return first tags before media data
return firstTags.removeFirst();
}
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();
IoBuffer body = IoBuffer.allocate(tag.getBodySize());
body.setAutoExpand(true);
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.free();
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;
}
/**
* Simple holder for id3 meta data
*/
static class MetaData {
String album = "";
String artist = "";
String genre = "";
String songName = "";
String track = "";
String year = "";
String comment = "";
byte[] covr = null;
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getGenre() {
return genre;
}
public void setGenre(String genre) {
this.genre = genre;
}
public String getSongName() {
return songName;
}
public void setSongName(String songName) {
this.songName = songName;
}
public String getTrack() {
return track;
}
public void setTrack(String track) {
this.track = track;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public byte[] getCovr() {
return covr;
}
public void setCovr(byte[] covr) {
this.covr = covr;
log.debug("Cover image array size: {}", covr.length);
}
public boolean hasCoverImage() {
return covr != null;
}
}
}