/* * Copyright (C) 2011 in-somnia * * This file is part of JAAD. * * JAAD 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 3 of the * License, or (at your option) any later version. * * JAAD 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, see <http://www.gnu.org/licenses/>. */ package net.sourceforge.jaad.mp4.api; import java.io.EOFException; import java.util.logging.Logger; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.logging.Level; import net.sourceforge.jaad.mp4.MP4InputStream; import net.sourceforge.jaad.mp4.boxes.Box; import net.sourceforge.jaad.mp4.boxes.BoxTypes; import net.sourceforge.jaad.mp4.boxes.impl.ChunkOffsetBox; import net.sourceforge.jaad.mp4.boxes.impl.DataEntryUrlBox; import net.sourceforge.jaad.mp4.boxes.impl.DataReferenceBox; import net.sourceforge.jaad.mp4.boxes.impl.MediaHeaderBox; import net.sourceforge.jaad.mp4.boxes.impl.SampleSizeBox; import net.sourceforge.jaad.mp4.boxes.impl.SampleToChunkBox; import net.sourceforge.jaad.mp4.boxes.impl.DecodingTimeToSampleBox; import net.sourceforge.jaad.mp4.boxes.impl.TrackHeaderBox; import net.sourceforge.jaad.mp4.od.DecoderSpecificInfo; import net.sourceforge.jaad.mp4.boxes.impl.ESDBox; import net.sourceforge.jaad.mp4.od.Descriptor; /** * This class represents a track in a movie. * * Each track contains either a decoder specific info as a byte array or a * <code>DecoderInfo</code> object that contains necessary information for the * decoder. * * @author in-somnia */ //TODO: expand javadoc; use generics for subclasses? public abstract class Track { public interface Codec { //TODO: currently only marker interface } private final MP4InputStream in; protected final TrackHeaderBox tkhd; private final MediaHeaderBox mdhd; private final boolean inFile; private final List<Frame> frames; private URL location; private int currentFrame; //info structures protected DecoderSpecificInfo decoderSpecificInfo; protected DecoderInfo decoderInfo; protected Protection protection; private int lastFramePadding; private DecodingTimeToSampleBox stts; Track(Box trak, MP4InputStream in) { this.in = in; tkhd = (TrackHeaderBox) trak.getChild(BoxTypes.TRACK_HEADER_BOX); final Box mdia = trak.getChild(BoxTypes.MEDIA_BOX); mdhd = (MediaHeaderBox) mdia.getChild(BoxTypes.MEDIA_HEADER_BOX); final Box minf = mdia.getChild(BoxTypes.MEDIA_INFORMATION_BOX); final Box dinf = minf.getChild(BoxTypes.DATA_INFORMATION_BOX); final DataReferenceBox dref = (DataReferenceBox) dinf.getChild(BoxTypes.DATA_REFERENCE_BOX); //TODO: support URNs if(dref.hasChild(BoxTypes.DATA_ENTRY_URL_BOX)) { DataEntryUrlBox url = (DataEntryUrlBox) dref.getChild(BoxTypes.DATA_ENTRY_URL_BOX); inFile = url.isInFile(); if(!inFile) { try { location = new URL(url.getLocation()); } catch(MalformedURLException e) { Logger.getLogger("MP4 API").log(Level.WARNING, "Parsing URL-Box failed: {0}, url: {1}", new String[]{e.toString(), url.getLocation()}); location = null; } } } /*else if(dref.containsChild(BoxTypes.DATA_ENTRY_URN_BOX)) { DataEntryUrnBox urn = (DataEntryUrnBox) dref.getChild(BoxTypes.DATA_ENTRY_URN_BOX); inFile = urn.isInFile(); location = urn.getLocation(); }*/ else { inFile = true; location = null; } //sample table final Box stbl = minf.getChild(BoxTypes.SAMPLE_TABLE_BOX); if(stbl.hasChildren()) { frames = new ArrayList<Frame>(); parseSampleTable(stbl); } else frames = Collections.emptyList(); parseGaplessInfo(); currentFrame = 0; } private void parseSampleTable(Box stbl) { final double timeScale = mdhd.getTimeScale(); final Type type = getType(); //sample sizes final long[] sampleSizes = ((SampleSizeBox) stbl.getChild(BoxTypes.SAMPLE_SIZE_BOX)).getSampleSizes(); //chunk offsets final ChunkOffsetBox stco; if(stbl.hasChild(BoxTypes.CHUNK_OFFSET_BOX)) stco = (ChunkOffsetBox) stbl.getChild(BoxTypes.CHUNK_OFFSET_BOX); else stco = (ChunkOffsetBox) stbl.getChild(BoxTypes.CHUNK_LARGE_OFFSET_BOX); final long[] chunkOffsets = stco.getChunks(); //samples to chunks final SampleToChunkBox stsc = ((SampleToChunkBox) stbl.getChild(BoxTypes.SAMPLE_TO_CHUNK_BOX)); final long[] firstChunks = stsc.getFirstChunks(); final long[] samplesPerChunk = stsc.getSamplesPerChunk(); //sample durations/timestamps stts = (DecodingTimeToSampleBox) stbl.getChild(BoxTypes.DECODING_TIME_TO_SAMPLE_BOX); final long[] sampleCounts = stts.getSampleCounts(); final long[] sampleDeltas = stts.getSampleDeltas(); final long[] timeOffsets = new long[sampleSizes.length]; long tmp = 0; int off = 0; for(int i = 0; i<sampleCounts.length; i++) { for(int j = 0; j<sampleCounts[i]; j++) { timeOffsets[off+j] = tmp; tmp += sampleDeltas[i]; } off += sampleCounts[i]; } //create samples int current = 0; int lastChunk; double timeStamp; long offset = 0; //iterate over all chunk groups for(int i = 0; i<firstChunks.length; i++) { if(i<firstChunks.length-1) lastChunk = (int) firstChunks[i+1]-1; else lastChunk = chunkOffsets.length; //iterate over all chunks in current group for(int j = (int) firstChunks[i]-1; j<lastChunk; j++) { offset = chunkOffsets[j]; //iterate over all samples in current chunk for(int k = 0; k<samplesPerChunk[i]; k++) { //create samples timeStamp = ((double) timeOffsets[current])/timeScale; frames.add(new Frame(type, offset, sampleSizes[current], timeStamp)); offset += sampleSizes[current]; current++; } } } //frames need not to be time-ordered: sort by timestamp //TODO: is it possible to add them to the specific position? Collections.sort(frames); } private void parseGaplessInfo() { // if stts has two entries, last one is usually padding if (stts != null && stts.getSampleDeltas().length == 2) { lastFramePadding = (int) stts.getSampleDeltas()[1]; } } //TODO: implement other entry descriptors protected void findDecoderSpecificInfo(ESDBox esds) { final Descriptor ed = esds.getEntryDescriptor(); final List<Descriptor> children = ed.getChildren(); List<Descriptor> children2; for(Descriptor e : children) { children2 = e.getChildren(); for(Descriptor e2 : children2) { switch(e2.getType()) { case Descriptor.TYPE_DECODER_SPECIFIC_INFO: decoderSpecificInfo = (DecoderSpecificInfo) e2; break; } } } } protected <T> void parseSampleEntry(Box sampleEntry, Class<T> clazz) { T type; try { type = clazz.newInstance(); if(sampleEntry.getClass().isInstance(type)) { System.out.println("true"); } } catch(InstantiationException ex) { ex.printStackTrace(); } catch(IllegalAccessException ex) { ex.printStackTrace(); } } public abstract Type getType(); public abstract Codec getCodec(); //tkhd /** * Returns true if the track is enabled. A disabled track is treated as if * it were not present. * @return true if the track is enabled */ public boolean isEnabled() { return tkhd.isTrackEnabled(); } /** * Returns true if the track is used in the presentation. * @return true if the track is used */ public boolean isUsed() { return tkhd.isTrackInMovie(); } /** * Returns true if the track is used in previews. * @return true if the track is used in previews */ public boolean isUsedForPreview() { return tkhd.isTrackInPreview(); } /** * Returns the time this track was created. * @return the creation time */ public Date getCreationTime() { return Utils.getDate(tkhd.getCreationTime()); } /** * Returns the last time this track was modified. * @return the modification time */ public Date getModificationTime() { return Utils.getDate(tkhd.getModificationTime()); } //mdhd /** * Returns the language for this media. * @return the language */ public Locale getLanguage() { return new Locale(mdhd.getLanguage()); } /** * Returns true if the data for this track is present in this file (stream). * If not, <code>getLocation()</code> returns the URL where the data can be * found. * @return true if the data is in this file (stream), false otherwise */ public boolean isInFile() { return inFile; } /** * If the data for this track is not present in this file (if * <code>isInFile</code> returns false), this method returns the data's * location. Else null is returned. * @return the data's location or null if the data is in this file */ public URL getLocation() { return location; } //info structures /** * Returns the decoder specific info, if present. It contains configuration * data for the decoder. If the decoder specific info is not present, the * track contains a <code>DecoderInfo</code>. * * @see #getDecoderInfo() * @return the decoder specific info */ public byte[] getDecoderSpecificInfo() { return decoderSpecificInfo.getData(); } /** * Returns the <code>DecoderInfo</code>, if present. It contains * configuration information for the decoder. If the structure is not * present, the track contains a decoder specific info. * * @see #getDecoderSpecificInfo() * @return the codec specific structure */ public DecoderInfo getDecoderInfo() { return decoderInfo; } /** * Returns the <code>ProtectionInformation</code> object that contains * details about the DRM system used. If no protection is present this * method returns null. * * @return a <code>ProtectionInformation</code> object or null if no * protection is used */ public Protection getProtection() { return protection; } //reading /** * Indicates if there are more frames to be read in this track. * * @return true if there is at least one more frame to read. */ public boolean hasMoreFrames() { return currentFrame<frames.size(); } /** * Reads the next frame from this track. If it contains no more frames to * read, null is returned. * * @return the next frame or null if there are no more frames to read * @throws IOException if reading fails */ public Frame readNextFrame() throws IOException { Frame frame = null; if(hasMoreFrames()) { frame = frames.get(currentFrame); final long diff = frame.getOffset()-in.getOffset(); if(diff>0) in.skipBytes(diff); else if(diff<0) { if(in.hasRandomAccess()) in.seek(frame.getOffset()); else { Logger.getLogger("MP4 API").log(Level.WARNING, "readNextFrame failed: frame {0} already skipped, offset:{1}, stream:{2}", new Object[]{currentFrame, frame.getOffset(), in.getOffset()}); throw new IOException("frame already skipped and no random access"); } } final byte[] b = new byte[(int) frame.getSize()]; try { in.readBytes(b); } catch(EOFException e) { Logger.getLogger("MP4 API").log(Level.WARNING, "readNextFrame failed: tried to read {0} bytes at {1}", new Long[]{frame.getSize(), in.getOffset()}); throw e; } frame.setData(b); currentFrame++; } return frame; } /** * This method tries to seek to the frame that is nearest to the given * timestamp. It returns the timestamp of the frame it seeked to or -1 if * none was found. * * @param timestamp a timestamp to seek to * @return the frame's timestamp that the method seeked to */ public double seek(double timestamp) { //find first frame > timestamp Frame frame = null; for(int i = 0; i<frames.size(); i++) { frame = frames.get(i); if(frame.getTime()>timestamp) { currentFrame = i - 1; break; } } return (frame==null) ? -1 : frames.get(currentFrame).getTime(); } /** * Returns the timestamp of the next frame to be read. This is needed to * read frames from a movie that contains multiple tracks. * * @return the next frame's timestamp */ double getNextTimeStamp() { return frames.get(currentFrame).getTime(); } /** * Returns duration of sample, based on stts data * @param sample sample number * @return duration in pcm samples */ public long getSampleDuration(int sample) { int co = 0; for (int i = 0; i < stts.getSampleCounts().length; i++) { long delta = stts.getSampleCounts()[i]; if (sample < co + delta) return stts.getSampleDeltas()[i]; co += delta; } return 0; } public int getLastFramePadding() { return lastFramePadding; } public void setCurrentFrame(int frame) { currentFrame = frame; } public int getFrameCount() { return frames.size(); } }