/* * JBoss, Home of Professional Open Source * Copyright 2011, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.restcomm.media.resource.player.video.mpeg; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; import java.util.Random; import org.apache.log4j.Logger; /** * * @author amit bhayani * */ public abstract class RTPTrack { private final Logger logger = Logger.getLogger(this.getClass()); private List<RTPLocalPacket> rtPktList = new ArrayList<RTPLocalPacket>(); private TrackBox hintTrackBox = null; private TrackBox trackBox = null; private RandomAccessFile hintTrackRAF = null; private RandomAccessFile trackRAF = null; private long[] hintSamplesOffSet = null; private long[] samplesOffSet = null; private long[] sampleDelta = null; private int period = 0; private int[] timeDelta = null; private volatile int hintSamplesSent = 0; private volatile int audioSamplesSent = 0; private volatile long rtpTimeStamp = Math.abs((new Random()).nextInt()); private String sdpText; private long trackId = -1; long hintSampleCount = 0; double npt; boolean first = true; public RTPTrack(TrackBox audioTrackBox, TrackBox audioHintTrackBox, File file) throws FileNotFoundException { long sampleCount = 0; SampleTableBox hintSampleTableBox = null; SampleTableBox sampleTableBox = null; long[] hintChunkOffset = null; long[] hintSamplesPerChunk = null; long[] hintEntrySize = null; long hintSampleSize = 0; int timeScale = 0; this.trackBox = audioTrackBox; this.hintTrackBox = audioHintTrackBox; this.hintTrackRAF = new RandomAccessFile(file, "r"); this.trackRAF = new RandomAccessFile(file, "r"); npt = (this.hintTrackBox.getMediaBox().getMediaHeaderBox().getDuration()) / (this.hintTrackBox.getMediaBox().getMediaHeaderBox().getTimescale()); // TODO : We are assuming that Hint Track is referencing only one Track, but there could be more. trackId = this.hintTrackBox.getTrackHeaderBox().getTrackID(); for (Box b : this.hintTrackBox.getUserDataBox().getUserDefinedBoxes()) { if (b.getType().equals(TrackHintInformation.TYPE_S)) { this.sdpText = ((TrackHintInformation) b).getRtpTrackSdpHintInformation().getSdpText(); } } hintSampleTableBox = this.hintTrackBox.getMediaBox().getMediaInformationBox().getSampleTableBox(); sampleTableBox = this.trackBox.getMediaBox().getMediaInformationBox().getSampleTableBox(); hintChunkOffset = hintSampleTableBox.getChunkOffsetBox().getChunkOffset(); long[] audioChunkOffset = sampleTableBox.getChunkOffsetBox().getChunkOffset(); if (logger.isDebugEnabled()) { logger.debug("Hint ChunkOffset length = " + hintChunkOffset.length); logger.debug("ChunkOffset length = " + audioChunkOffset.length); } for (SampleEntry sampleEntry : hintSampleTableBox.getSampleDescription().getSampleEntries()) { if (sampleEntry.getType().equals(RtpHintSampleEntry.TYPE_S)) { for (Box box : ((RtpHintSampleEntry) sampleEntry).getAdditionaldata()) { if (box.getType().equals(TimeScaleEntry.TYPE_S)) { timeScale = ((TimeScaleEntry) box).getTimeScale(); if (logger.isDebugEnabled()) { logger.debug("timeScale = " + timeScale); } } } } } hintSampleCount = hintSampleTableBox.getSampleSizeBox().getSampleCount(); sampleCount = sampleTableBox.getSampleSizeBox().getSampleCount(); TimeToSampleBox timeToSampleBox = hintSampleTableBox.getTimeToSampleBox(); if (timeToSampleBox.getEntryCount() == 1) { // this.heartBeat = (int) (this.duration / this.timeScale * 1000) / this.sampleCount; this.period = (int) (((float) timeToSampleBox.getSampleDelta()[0] / (float) timeScale) * 1000); } else { // heart beat different for each sample int heartBeatArrCount = 0; this.timeDelta = new int[(int) hintSampleCount]; long[] sampleCountArr = timeToSampleBox.getSampleCount(); long[] sampleDelta = timeToSampleBox.getSampleDelta(); for (int i = 0; i < sampleCountArr.length; i++) { long temp = sampleCountArr[i]; timeDelta[heartBeatArrCount++] = (int) (((float) sampleDelta[i] / (float) timeScale) * 1000); for (int j = 1; j < temp; j++) { timeDelta[heartBeatArrCount++] = (int) sampleDelta[i]; } } } this.hintSamplesOffSet = new long[(int) hintSampleCount]; this.samplesOffSet = new long[(int) sampleCount]; if (logger.isDebugEnabled()) { logger.debug("Heart Beat = " + this.period); if (this.period == 0) { logger.debug("heartBeat zero so use heartBeatArr. Length = " + timeDelta.length); } } hintSamplesPerChunk = new long[hintChunkOffset.length]; long[] audioSamplesPerChunk = new long[audioChunkOffset.length]; // Calculate the Number of Samples for each Chunk for Hint Audio Track int samplesPerChunkCount = 0; long[] hintFirstChunk = hintSampleTableBox.getSampleToChunkBox().getFirstChunk(); long[] hintSamplesPerChunkTemp = hintSampleTableBox.getSampleToChunkBox().getSamplesPerChunk(); long samplesAtChunk; for (int i = 0; i < (hintFirstChunk.length - 1); i++) { long temp = (hintFirstChunk[i + 1] - hintFirstChunk[i]); samplesAtChunk = hintSamplesPerChunkTemp[i]; for (int j = 0; j < temp; j++) { hintSamplesPerChunk[samplesPerChunkCount++] = samplesAtChunk; } } samplesAtChunk = hintSamplesPerChunkTemp[(hintFirstChunk.length - 1)]; for (int j = samplesPerChunkCount; j < hintSamplesPerChunk.length; j++) { hintSamplesPerChunk[samplesPerChunkCount++] = samplesAtChunk; } // Calculate the Number of Samples for each Chunk for Audio Track samplesPerChunkCount = 0; long[] audioFirstChunk = sampleTableBox.getSampleToChunkBox().getFirstChunk(); long[] audioSamplesPerChunkTemp = sampleTableBox.getSampleToChunkBox().getSamplesPerChunk(); for (int i = 0; i < (audioFirstChunk.length - 1); i++) { long temp = (audioFirstChunk[i + 1] - audioFirstChunk[i]); samplesAtChunk = audioSamplesPerChunkTemp[i]; for (int j = 0; j < temp; j++) { audioSamplesPerChunk[samplesPerChunkCount++] = samplesAtChunk; } } samplesAtChunk = audioSamplesPerChunkTemp[(audioFirstChunk.length - 1)]; for (int j = samplesPerChunkCount; j < audioSamplesPerChunk.length; j++) { audioSamplesPerChunk[samplesPerChunkCount++] = samplesAtChunk; } // This is debug if (logger.isDebugEnabled()) { int tempCnt = 0; for (int i = 0; i < hintSamplesPerChunk.length; i++) { tempCnt += hintSamplesPerChunk[i]; } logger.debug("Total sample count for Hint Track that should match with SampleSizeBox sampleCount(" + hintSampleCount + ") = " + tempCnt); tempCnt = 0; for (int i = 0; i < audioSamplesPerChunk.length; i++) { tempCnt += audioSamplesPerChunk[i]; } logger.debug("Total sample count for Track that should match with SampleSizeBox sampleCount(" + sampleTableBox.getSampleSizeBox().getSampleCount() + ") = " + tempCnt); }// Debug ends // Calculate the OffSet for each Sample for Hint Track hintSampleSize = hintSampleTableBox.getSampleSizeBox().getSampleSize(); hintEntrySize = hintSampleTableBox.getSampleSizeBox().getEntrySize(); int samplesOffSetCount = 0; for (int i = 0; i < hintChunkOffset.length; i++) { long chunkOffForChunk = hintChunkOffset[i]; long samplesInThisChunk = hintSamplesPerChunk[i]; hintSamplesOffSet[samplesOffSetCount++] = chunkOffForChunk; for (int j = 1; j < samplesInThisChunk; j++) { if (hintSampleSize == 0) { hintSamplesOffSet[samplesOffSetCount++] = hintSamplesOffSet[samplesOffSetCount - 2] + hintEntrySize[samplesOffSetCount - 2]; } else { hintSamplesOffSet[samplesOffSetCount++] = (chunkOffForChunk + hintSampleSize * j); } } } // debug // if (logger.isDebugEnabled()) { // logger.info("Each Sample Off Set for Hint Track "); // StringBuffer b = new StringBuffer(); // for (int i = 0; i < hintSamplesOffSet.length; i++) { // b.append(hintSamplesOffSet[i]).append(","); // } // logger.debug(b.toString()); // }// debug ends // Calculate the OffSet for each Sample for Audio Track long audioSampleSize = sampleTableBox.getSampleSizeBox().getSampleSize(); long[] audioEntrySize = sampleTableBox.getSampleSizeBox().getEntrySize(); samplesOffSetCount = 0; for (int i = 0; i < audioChunkOffset.length; i++) { long chunkOffForChunk = audioChunkOffset[i]; long samplesInThisChunk = audioSamplesPerChunk[i]; samplesOffSet[samplesOffSetCount++] = chunkOffForChunk; for (int j = 1; j < samplesInThisChunk; j++) { if (audioSampleSize == 0) { samplesOffSet[samplesOffSetCount++] = samplesOffSet[samplesOffSetCount - 2] + audioEntrySize[samplesOffSetCount - 2]; } else { samplesOffSet[samplesOffSetCount++] = (chunkOffForChunk + audioSampleSize * j); } } } // debug. Let us keep it till 3000 only else it will eat away all memory // if (logger.isDebugEnabled() && (samplesOffSet.length < 3000)) { // logger.info("Each Sample Off Set for Track "); // StringBuffer b = new StringBuffer(); // for (int i = 0; i < this.samplesOffSet.length; i++) { // b.append(this.samplesOffSet[i]).append(","); // } // logger.debug(b.toString()); // }// debug ends // Calculate SampleDelta for each sample sampleDelta = new long[(int) hintSampleCount]; long[] hintSampleCountArr = hintSampleTableBox.getTimeToSampleBox().getSampleCount(); long[] hintSampleDeltaArr = hintSampleTableBox.getTimeToSampleBox().getSampleDelta(); samplesOffSetCount = 0; for (int i = 0; i < hintSampleCountArr.length; i++) { long smplCnt = hintSampleCountArr[i]; long smplDelts = hintSampleDeltaArr[i]; for (int j = 0; j < (int) smplCnt; j++) { sampleDelta[samplesOffSetCount++] = smplDelts; } } } public void setRtpTime(long rtpTime) { this.rtpTimeStamp = rtpTime; } public RTPSample process() throws IOException { RTPSample rtpSample = null; if (this.hintSampleCount == this.hintSamplesSent) { return null; } rtpSample = new RTPSample(); RTPLocalPacket rtpPacket = null; long hintSampleOffset = hintSamplesOffSet[hintSamplesSent]; long audioSampleOffSet = samplesOffSet[audioSamplesSent]; this.hintTrackRAF.seek(hintSampleOffset); int packetCount = (this.hintTrackRAF.read() << 8 | this.hintTrackRAF.read()); rtpSample.setPacketCount(packetCount); if (packetCount > 0) { this.audioSamplesSent++; } // reserved this.hintTrackRAF.skipBytes(2); if (first) { first = false; } else { rtpTimeStamp += sampleDelta[hintSamplesSent]; } for (int i = 0; i < packetCount; i++) { rtpPacket = new RTPLocalPacket(); rtpPacket.load(this.hintTrackRAF); ByteArrayOutputStream bos = new ByteArrayOutputStream(); // Now load the Payload for (RTPConstructor rtpCons : rtpPacket.RTPConstructorList()) { switch (rtpCons.getConstructorType()) { case RTPNoOpConstructor.TYPE: // TODO : Anything to do here? break; case RTPImmediateConstructor.TYPE: byte[] data = ((RTPImmediateConstructor) rtpCons).getData(); bos.write(data, 0, data.length); break; case RTPSampleConstructor.TYPE: RTPSampleConstructor rtpSampCons = (RTPSampleConstructor) rtpCons; // TODO : Can we avoid creating the byte[] again here? byte[] rtpPaylod = null; // TODO : Verify if this is correct? /* * From page 76 of ISO/IEC 14496-12 * * For hint tracks where the media is sent �?in the clear’, the sample entry then specifies the * bytes to copy from the media track, by giving the sample number, data offset, and length to copy. * The track reference may index into the table of track references (a strictly positive value), * name the hint track itself (-1), or the only associated media track (0). (The value zero is * therefore equivalent to the value 1.) * * I couldn't make if bellow is what they are saying? Why do they talk so cryptic? */ if (rtpSampCons.getTrackRefIndex() == -1) { this.hintTrackRAF.seek((hintSampleOffset + rtpSampCons.getSampleOffSet())); rtpPaylod = new byte[rtpSampCons.getLength()]; this.hintTrackRAF.read(rtpPaylod, 0, rtpSampCons.getLength()); } else { // TODO : Should OffSet be added even if we are referring to non rtp mdat? this.trackRAF.seek(audioSampleOffSet + rtpSampCons.getSampleOffSet()); rtpPaylod = new byte[rtpSampCons.getLength()]; int read = this.trackRAF.read(rtpPaylod, 0, rtpSampCons.getLength()); } bos.write(rtpPaylod, 0, rtpPaylod.length); break; case RTPSampleDescriptionConstructor.TYPE: // TODO : What here? break; } } rtpPacket.setPayload(bos.toByteArray()); rtpPacket.setRtpTimestamp(this.rtpTimeStamp); rtpSample.addRtpLocalPackets(rtpPacket); }// for if (this.period == 0.0) { rtpSample.setSamplePeriod(this.timeDelta[hintSamplesSent]); } else { rtpSample.setSamplePeriod(this.period); } hintSamplesSent++; // FIXME Do we care for extraByte? return rtpSample; } public void close() { try { if (this.hintTrackRAF != null) { this.hintTrackRAF.close(); } if (this.trackRAF != null) { this.trackRAF.close(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.hintTrackRAF = null; this.trackRAF = null; this.hintTrackBox = null; this.trackBox = null; this.hintSamplesOffSet = null; this.samplesOffSet = null; this.timeDelta = null; } public String getSdpText() { return sdpText; } public long getTrackId() { return this.trackId; } public float getPacketPeriod() { return this.period; } public float getHeartBeat() { return this.period; } public double getNPT() { return this.npt; } }