/**
*
*/
package mp4.util.atom;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import mp4.util.MP4Log;
/**
* The movie atom is a top-level atom. It contains the metadata for a presentation.
*/
public class MoovAtom extends ContainerAtom {
// the movie header atom
private MvhdAtom mvhd;
// initial object descriptor
private IodsAtom iods;
// the user data atom
private UdtaAtom udta;
// the list of tracks
private List<TrakAtom> traks;
/**
* Constructor for movie atom
*/
public MoovAtom() {
super(new byte[]{'m', 'o', 'o', 'v'});
}
/**
* Copy constructor for movie atom. Performs a deep copy.
* @param old the movie atom to copy
*/
public MoovAtom(MoovAtom old) {
super(old);
mvhd = new MvhdAtom(old.mvhd);
if (iods != null) {
iods = new IodsAtom(old.iods);
}
if (udta != null) {
udta = new UdtaAtom(old.udta);
}
traks = new LinkedList<TrakAtom>();
for (Iterator<TrakAtom> i = old.getTracks(); i.hasNext();) {
traks.add(new TrakAtom(i.next()));
}
}
/**
* Return the movie header atom
* @return the movie header atom
*/
public MvhdAtom getMvhd() {
return mvhd;
}
/**
* Set the movie header atom
* @param mvhd the new movie header atom
*/
public void setMvhd(MvhdAtom mvhd) {
this.mvhd = mvhd;
}
/**
* Return the initial object descriptor atom
* @return the initial object descriptor atom
*/
public IodsAtom getIods() {
return iods;
}
/**
* Set the initial object descriptor atom
* @param iods the new initial object descriptor
*/
public void setIods(IodsAtom iods) {
this.iods = iods;
}
/**
* Return the user data atom
* @return the user data atom
*/
public UdtaAtom getUdta() {
return udta;
}
/**
* Set the user-data atom
* @param udta the new user-data atom.
*/
public void setUdta(UdtaAtom udta) {
this.udta = udta;
}
/**
* Return an iterator with the media's tracks. For most movies, there are two tracks, the sound
* track and the video track.
* @return an iterator with the movie traks.
*/
public Iterator<TrakAtom> getTracks() {
return traks.iterator();
}
/**
* Return the number of traks in this moov
* @return number of tracks
*/
public int getTrackCount() {
return traks.size();
}
/**
* Return the specified trak
* @return a TrakAtom for the requested trak
*/
public TrakAtom getTrack(int index) {
return traks.get(index);
}
/**
* Add a track to the movie. If no tracks have been added, then allocate
* space for the tracks.
* @param trak a new track.
*/
public void addTrack(TrakAtom trak) {
if (this.traks == null) {
this.traks = new LinkedList<TrakAtom>();
}
this.traks.add(trak);
}
/**
* Add a child atom to the moov atom. If the atom is not recognized as a child of moov
* then a run-time exception is thrown.
* @param atom the atom to add
*/
@Override
public void addChild(Atom atom) {
if (atom instanceof MvhdAtom) {
mvhd = (MvhdAtom) atom;
} else if (atom instanceof IodsAtom) {
iods = (IodsAtom) atom;
} else if (atom instanceof UdtaAtom) {
udta = (UdtaAtom) atom;
} else if (atom instanceof TrakAtom) {
if (traks == null) {
traks = new LinkedList<TrakAtom>();
}
traks.add((TrakAtom) atom);
} else {
//throw new AtomError("Can't add " + atom + " to moov");
addUnknownChild(atom);
}
}
/**
* Recompute the size of the moov atom, which needs to be done if
* any of the child atom sizes have changed.
*/
@Override
public void recomputeSize() {
long newSize = mvhd.size();
for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
newSize += i.next().size();
}
if (iods != null) {
newSize += iods.size();
}
if (udta != null) {
newSize += udta.size();
}
newSize += unknownChildrenSize();
setSize(ATOM_HEADER_SIZE + newSize);
}
public long normalizedTime(float seekTime) {
long firstByte = firstDataByteOffset();
long movieTimeScale = mvhd.getTimeScale();
long duration = mvhd.getDuration();
TimePos tp = findAdjustedTime(seekTime);
long normalizedTime = (long) ((float) tp.pos / (float) tp.scale);
return normalizedTime;
}
public int timeOffset(int byteOffset) {
long movieTimeScale = mvhd.getTimeScale();
long duration = mvhd.getDuration();
return 0;
}
public static class TimePos {
public long pos;
public long scale;
}
/**
* Cut the movie atom at the specified time
* @param time the time at which the cut is performed. Must be converted to movie time.
* @return the new movie atom
*/
public MoovAtom cut(float time) {
long movieTimeScale = mvhd.getTimeScale();
long duration = mvhd.getDuration();
TimePos tp = findAdjustedTime(time);
time = (float) tp.pos / (float) tp.scale;
MP4Log.log("DBG: Movie time " + (duration / movieTimeScale) + " sec, cut at " + time + "sec");
MP4Log.log("\tDBG: ts " + movieTimeScale + " cut at " + (time * movieTimeScale));
MoovAtom cutMoov = new MoovAtom();
cutMoov.setMvhd(mvhd.cut());
if (iods != null) {
cutMoov.setIods(iods.cut());
}
if (udta != null) {
cutMoov.setUdta(udta.cut());
}
long minDuration = Long.MAX_VALUE;
/*
// Make sure traks with stss atom go first
List<TrakAtom> noStssTrakList = new ArrayList<TrakAtom>();
List<TrakAtom> trakList = new ArrayList<TrakAtom>();
for (Iterator<TrakAtom> i = getTracks(); i.hasNext(); ) {
TrakAtom a = i.next();
if (a.getMdia().getMinf().getStbl().getStss() != null)
trakList.add(a);
else
noStssTrakList.add(a);
}
trakList.addAll(noStssTrakList);
Iterator<TrakAtom> trakIterator = trakList.iterator();
*/
Iterator<TrakAtom> trakIterator = getTracks();
// iterate over each track and cut the track
for (Iterator<TrakAtom> i = trakIterator; i.hasNext();) {
TrakAtom cutTrak = i.next().cut(tp.pos, tp.scale);
cutMoov.addTrack(cutTrak);
// need to convert the media time-scale to the movie time-scale
long cutDuration = cutTrak.convertDuration(movieTimeScale);
MP4Log.log("DBG: cutDuration " + cutDuration);
cutTrak.fixupDuration(cutDuration);
if (cutDuration > cutMoov.getMvhd().getDuration()) {
cutMoov.getMvhd().setDuration(cutDuration);
}
if (cutDuration < minDuration) {
minDuration = cutDuration;
}
//time = (duration - cutDuration) / (float) movieTimeScale;
MP4Log.log("DBG: new time " + time);
}
// check if any edits need to be added
/* for (Iterator<TrakAtom> i = cutMoov.getTracks(); i.hasNext(); ) {
TrakAtom trak = i.next();
long trakDuration = trak.convertDuration(movieTimeScale);
MP4Log.log("DBG: trak duration " + trakDuration);
if (trakDuration > minDuration) {
long editDuration = trak.convertToMediaScale(trakDuration - minDuration, movieTimeScale);
MP4Log.log("\tDBG: edit duration " + editDuration);
// add an edit to the media
trak.addEdit(editDuration);
trak.recomputeSize();
}
}
*/
cutMoov.recomputeSize();
return cutMoov;
}
/**
*/
public TimePos findAdjustedTime(float time) {
float adjustedTime = Float.MAX_VALUE;
TimePos tp = new TimePos();
boolean hasStts = false;
for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
TrakAtom trak = i.next();
StblAtom stbl = trak.getMdia().getMinf().getStbl();
long mediaTimeScale = trak.getMdia().getMdhd().getTimeScale();
long mediaTime = (long) (time * mediaTimeScale);
boolean isStts = stbl.getStss() != null;
if (isStts) {
long sampleNum = stbl.getStts().timeToSample(mediaTime);
MP4Log.log("DBG: sampleNum " + sampleNum);
sampleNum = stbl.getStss().getKeyFrame(sampleNum);
MP4Log.log("DBG: new key frame " + sampleNum);
mediaTime = stbl.getStts().sampleToTime(sampleNum);
}
float realTime = (float) mediaTime / mediaTimeScale;
if ((realTime < adjustedTime && !hasStts) || isStts) {
tp.pos = mediaTime;
tp.scale = mediaTimeScale;
adjustedTime = realTime;
}
hasStts = isStts;
String tt = "Unk";
if (trak.getMdia().getHdlr().isVideo()) {
tt = "Vid";
} else if (trak.getMdia().getHdlr().isSound()) {
tt = "Snd";
}
MP4Log.log("DBG: trackType " + tt + " " + (isStts ? "(stts) " : "") + "trackTime: " + realTime + " adjustTime: " + adjustedTime + " mediaTime(" + mediaTime + "/" + mediaTimeScale + ") adjTimePos(" + tp.pos + "/" + tp.scale + ")");
//" spec time " + (long)(time * mediaTimeScale) + " adj time " + mediaTime +
//" spec time sec " + (long)(time) + " adj time sec " + (mediaTime/mediaTimeScale));
}
return tp;
}
/**
* Return the byte offset of the first data in the mdat atom.
* This is computed by looking at the first entry in the stco atom,
* which contains mdat offset values. This method returns the smallest
* value of any of the tracks.
* @return the byte offset of the first data.
*/
public long firstDataByteOffset() {
long offset = Long.MAX_VALUE;
for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
StcoAtom stco = i.next().getMdia().getMinf().getStbl().getStco();
if (stco.getChunkOffset(1) < offset) {
offset = stco.getChunkOffset(1);
}
}
return offset;
}
// public long getSampleNum(long offset) {
// long sampleNum;
// for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
// StcoAtom stco = i.next().getMdia().getMinf().getStbl().getStco();
// if (i.next().getMdia(). > offset && stco.size()) {
//// offset = stco.getChunkOffset(1);
// }
// }
// return offset;
// }
@Override
public void accept(AtomVisitor v) throws AtomException {
v.visit(this);
}
/**
* Write the moov atom data to the specified output
* @param out where the data goes
* @throws IOException if there is an error writing the data
*/
@Override
public void writeData(DataOutput out) throws IOException {
writeHeader(out);
mvhd.writeData(out);
if (iods != null) {
iods.writeData(out);
}
for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
i.next().writeData(out);
}
if (udta != null) {
udta.writeData(out);
}
writeUnknownChildren(out);
}
/**
* Update the fixed offset values in the atom. This needs to be done if
* the file contents change.
* @param delta the change in file size
*/
public void fixupOffsets(long delta) {
for (Iterator<TrakAtom> i = getTracks(); i.hasNext();) {
i.next().fixupOffsets(delta);
}
}
}