/**********
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. (See <http://www.gnu.org/copyleft/lesser.html>.)

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.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
**********/
// "liveMedia"
// Copyright (c) 1996-2016 Live Networks, Inc.  All rights reserved.
// A sink that generates a QuickTime file from a composite media session
// Implementation

#include "QuickTimeFileSink.hh"
#include "QuickTimeGenericRTPSource.hh"
#include "GroupsockHelper.hh"
#include "InputFile.hh"
#include "OutputFile.hh"
#include "H263plusVideoRTPSource.hh" // for the special header
#include "MPEG4GenericRTPSource.hh" //for "samplingFrequencyFromAudioSpecificConfig()"
#include "MPEG4LATMAudioRTPSource.hh" // for "parseGeneralConfigStr()"
#include "Base64.hh"

#include <ctype.h>

#define fourChar(x,y,z,w) ( ((x)<<24)|((y)<<16)|((z)<<8)|(w) )

#define H264_IDR_FRAME 0x65  //bit 8 == 0, bits 7-6 (ref) == 3, bits 5-0 (type) == 5

////////// SubsessionIOState, ChunkDescriptor ///////////
// A structure used to represent the I/O state of each input 'subsession':

class ChunkDescriptor {
public:
  ChunkDescriptor(int64_t offsetInFile, unsigned size,
		  unsigned frameSize, unsigned frameDuration,
		  struct timeval presentationTime);

  ChunkDescriptor* extendChunk(int64_t newOffsetInFile, unsigned newSize,
			       unsigned newFrameSize,
			       unsigned newFrameDuration,
			       struct timeval newPresentationTime);
      // this may end up allocating a new chunk instead
public:
  ChunkDescriptor* fNextChunk;
  int64_t fOffsetInFile;
  unsigned fNumFrames;
  unsigned fFrameSize;
  unsigned fFrameDuration;
  struct timeval fPresentationTime; // of the start of the data
};

class SubsessionBuffer {
public:
  SubsessionBuffer(unsigned bufferSize)
    : fBufferSize(bufferSize) {
    reset();
    fData = new unsigned char[bufferSize];
  }
  virtual ~SubsessionBuffer() { delete[] fData; }
  void reset() { fBytesInUse = 0; }
  void addBytes(unsigned numBytes) { fBytesInUse += numBytes; }

  unsigned char* dataStart() { return &fData[0]; }
  unsigned char* dataEnd() { return &fData[fBytesInUse]; }
  unsigned bytesInUse() const { return fBytesInUse; }
  unsigned bytesAvailable() const { return fBufferSize - fBytesInUse; }

  void setPresentationTime(struct timeval const& presentationTime) {
    fPresentationTime = presentationTime;
  }
  struct timeval const& presentationTime() const {return fPresentationTime;}

private:
  unsigned fBufferSize;
  struct timeval fPresentationTime;
  unsigned char* fData;
  unsigned fBytesInUse;
};

class SyncFrame {
public:
  SyncFrame(unsigned frameNum);

public:
  class SyncFrame *nextSyncFrame;
  unsigned sfFrameNum;  
};

// A 64-bit counter, used below:
class Count64 {
public:
  Count64()
    : hi(0), lo(0) {
  }

  void operator+=(unsigned arg);

  u_int32_t hi, lo;
};

class SubsessionIOState {
public:
  SubsessionIOState(QuickTimeFileSink& sink, MediaSubsession& subsession);
  virtual ~SubsessionIOState();

  Boolean setQTstate();
  void setFinalQTstate();

  void afterGettingFrame(unsigned packetDataSize,
			 struct timeval presentationTime);
  void onSourceClosure();

  Boolean syncOK(struct timeval presentationTime);
      // returns true iff data is usable despite a sync check

  static void setHintTrack(SubsessionIOState* hintedTrack,
			   SubsessionIOState* hintTrack);
  Boolean isHintTrack() const { return fTrackHintedByUs != NULL; }
  Boolean hasHintTrack() const { return fHintTrackForUs != NULL; }

  UsageEnvironment& envir() const { return fOurSink.envir(); }

public:
  static unsigned fCurrentTrackNumber;
  unsigned fTrackID;
  SubsessionIOState* fHintTrackForUs; SubsessionIOState* fTrackHintedByUs;

  SubsessionBuffer *fBuffer, *fPrevBuffer;
  QuickTimeFileSink& fOurSink;
  MediaSubsession& fOurSubsession;

  unsigned short fLastPacketRTPSeqNum;
  Boolean fOurSourceIsActive;

  Boolean fHaveBeenSynced; // used in synchronizing with other streams
  struct timeval fSyncTime;

  Boolean fQTEnableTrack;
  unsigned fQTcomponentSubtype;
  char const* fQTcomponentName;
  typedef unsigned (QuickTimeFileSink::*atomCreationFunc)();
  atomCreationFunc fQTMediaInformationAtomCreator;
  atomCreationFunc fQTMediaDataAtomCreator;
  char const* fQTAudioDataType;
  unsigned short fQTSoundSampleVersion;
  unsigned fQTTimeScale;
  unsigned fQTTimeUnitsPerSample;
  unsigned fQTBytesPerFrame;
  unsigned fQTSamplesPerFrame;
  // These next fields are derived from the ones above,
  // plus the information from each chunk:
  unsigned fQTTotNumSamples;
  unsigned fQTDurationM; // in media time units
  unsigned fQTDurationT; // in track time units
  int64_t fTKHD_durationPosn;
      // position of the duration in the output 'tkhd' atom
  unsigned fQTInitialOffsetDuration;
      // if there's a pause at the beginning

  ChunkDescriptor *fHeadChunk, *fTailChunk;
  unsigned fNumChunks;
  SyncFrame *fHeadSyncFrame, *fTailSyncFrame;

  // Counters to be used in the hint track's 'udta'/'hinf' atom;
  struct hinf {
    Count64 trpy;
    Count64 nump;
    Count64 tpyl;
    // Is 'maxr' needed? Computing this would be a PITA. #####
    Count64 dmed;
    Count64 dimm;
    // 'drep' is always 0
    // 'tmin' and 'tmax' are always 0
    unsigned pmax;
    unsigned dmax;
  } fHINF;

private:
  void useFrame(SubsessionBuffer& buffer);
  void useFrameForHinting(unsigned frameSize,
			  struct timeval presentationTime,
			  unsigned startSampleNumber);

  // used by the above two routines:
  unsigned useFrame1(unsigned sourceDataSize,
		     struct timeval presentationTime,
		     unsigned frameDuration, int64_t destFileOffset);
      // returns the number of samples in this data

private:
  // A structure used for temporarily storing frame state:
  struct {
    unsigned frameSize;
    struct timeval presentationTime;
    int64_t destFileOffset; // used for non-hint tracks only

    // The remaining fields are used for hint tracks only:
    unsigned startSampleNumber;
    unsigned short seqNum;
    unsigned rtpHeader;
    unsigned char numSpecialHeaders; // used when our RTP source has special headers
    unsigned specialHeaderBytesLength; // ditto
    unsigned char specialHeaderBytes[SPECIAL_HEADER_BUFFER_SIZE]; // ditto
    unsigned packetSizes[256];
  } fPrevFrameState;
};


////////// QuickTimeFileSink implementation //////////

QuickTimeFileSink::QuickTimeFileSink(UsageEnvironment& env,
				     MediaSession& inputSession,
				     char const* outputFileName,
				     unsigned bufferSize,
				     unsigned short movieWidth,
				     unsigned short movieHeight,
				     unsigned movieFPS,
				     Boolean packetLossCompensate,
				     Boolean syncStreams,
				     Boolean generateHintTracks,
				     Boolean generateMP4Format)
  : Medium(env), fInputSession(inputSession),
    fBufferSize(bufferSize), fPacketLossCompensate(packetLossCompensate),
    fSyncStreams(syncStreams), fGenerateMP4Format(generateMP4Format),
    fAreCurrentlyBeingPlayed(False),
    fLargestRTPtimestampFrequency(0),
    fNumSubsessions(0), fNumSyncedSubsessions(0),
    fHaveCompletedOutputFile(False),
    fMovieWidth(movieWidth), fMovieHeight(movieHeight),
    fMovieFPS(movieFPS), fMaxTrackDurationM(0) {
  fOutFid = OpenOutputFile(env, outputFileName);
  if (fOutFid == NULL) return;

  fNewestSyncTime.tv_sec = fNewestSyncTime.tv_usec = 0;
  fFirstDataTime.tv_sec = fFirstDataTime.tv_usec = (unsigned)(~0);

  // Set up I/O state for each input subsession:
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    // Ignore subsessions without a data source:
    FramedSource* subsessionSource = subsession->readSource();
    if (subsessionSource == NULL) continue;

    // If "subsession's" SDP description specified screen dimension
    // or frame rate parameters, then use these.  (Note that this must
    // be done before the call to "setQTState()" below.)
    if (subsession->videoWidth() != 0) {
      fMovieWidth = subsession->videoWidth();
    }
    if (subsession->videoHeight() != 0) {
      fMovieHeight = subsession->videoHeight();
    }
    if (subsession->videoFPS() != 0) {
      fMovieFPS = subsession->videoFPS();
    }

    SubsessionIOState* ioState
      = new SubsessionIOState(*this, *subsession);
    if (ioState == NULL || !ioState->setQTstate()) {
      // We're not able to output a QuickTime track for this subsession
      delete ioState; ioState = NULL;
      continue;
    }
    subsession->miscPtr = (void*)ioState;

    if (generateHintTracks) {
      // Also create a hint track for this track:
      SubsessionIOState* hintTrack
	= new SubsessionIOState(*this, *subsession);
      SubsessionIOState::setHintTrack(ioState, hintTrack);
      if (!hintTrack->setQTstate()) {
	delete hintTrack;
	SubsessionIOState::setHintTrack(ioState, NULL);
      }
    }

    // Also set a 'BYE' handler for this subsession's RTCP instance:
    if (subsession->rtcpInstance() != NULL) {
      subsession->rtcpInstance()->setByeHandler(onRTCPBye, ioState);
    }

    unsigned rtpTimestampFrequency = subsession->rtpTimestampFrequency();
    if (rtpTimestampFrequency > fLargestRTPtimestampFrequency) {
      fLargestRTPtimestampFrequency = rtpTimestampFrequency;
    }

    ++fNumSubsessions;
  }

  // Use the current time as the file's creation and modification
  // time.  Use Apple's time format: seconds since January 1, 1904

  gettimeofday(&fStartTime, NULL);
  fAppleCreationTime = fStartTime.tv_sec - 0x83dac000;

  // Begin by writing a "mdat" atom at the start of the file.
  // (Later, when we've finished copying data to the file, we'll come
  // back and fill in its size.)
  fMDATposition = TellFile64(fOutFid);
  addAtomHeader64("mdat");
  // add 64Bit offset
  fMDATposition += 8;
}

QuickTimeFileSink::~QuickTimeFileSink() {
  completeOutputFile();

  // Then, stop streaming and delete each active "SubsessionIOState":
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    if (subsession->readSource() != NULL) subsession->readSource()->stopGettingFrames();

    SubsessionIOState* ioState
      = (SubsessionIOState*)(subsession->miscPtr);
    if (ioState == NULL) continue;

    delete ioState->fHintTrackForUs; // if any
    delete ioState;
  }

  // Finally, close our output file:
  CloseOutputFile(fOutFid);
}

QuickTimeFileSink*
QuickTimeFileSink::createNew(UsageEnvironment& env,
			     MediaSession& inputSession,
			     char const* outputFileName,
			     unsigned bufferSize,
			     unsigned short movieWidth,
			     unsigned short movieHeight,
			     unsigned movieFPS,
			     Boolean packetLossCompensate,
			     Boolean syncStreams,
			     Boolean generateHintTracks,
			     Boolean generateMP4Format) {
  QuickTimeFileSink* newSink = 
    new QuickTimeFileSink(env, inputSession, outputFileName, bufferSize, movieWidth, movieHeight, movieFPS,
			  packetLossCompensate, syncStreams, generateHintTracks, generateMP4Format);
  if (newSink == NULL || newSink->fOutFid == NULL) {
    Medium::close(newSink);
    return NULL;
  }

  return newSink;
}

void QuickTimeFileSink
::noteRecordedFrame(MediaSubsession& /*inputSubsession*/,
		    unsigned /*packetDataSize*/, struct timeval const& /*presentationTime*/) {
  // Default implementation: Do nothing
}

Boolean QuickTimeFileSink::startPlaying(afterPlayingFunc* afterFunc,
					void* afterClientData) {
  // Make sure we're not already being played:
  if (fAreCurrentlyBeingPlayed) {
    envir().setResultMsg("This sink has already been played");
    return False;
  }

  fAreCurrentlyBeingPlayed = True;
  fAfterFunc = afterFunc;
  fAfterClientData = afterClientData;

  return continuePlaying();
}

Boolean QuickTimeFileSink::continuePlaying() {
  // Run through each of our input session's 'subsessions',
  // asking for a frame from each one:
  Boolean haveActiveSubsessions = False;
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    FramedSource* subsessionSource = subsession->readSource();
    if (subsessionSource == NULL) continue;

    if (subsessionSource->isCurrentlyAwaitingData()) continue;

    SubsessionIOState* ioState
      = (SubsessionIOState*)(subsession->miscPtr);
    if (ioState == NULL) continue;

    haveActiveSubsessions = True;
    unsigned char* toPtr = ioState->fBuffer->dataEnd();
    unsigned toSize = ioState->fBuffer->bytesAvailable();
    subsessionSource->getNextFrame(toPtr, toSize,
				   afterGettingFrame, ioState,
				   onSourceClosure, ioState);
  }
  if (!haveActiveSubsessions) {
    envir().setResultMsg("No subsessions are currently active");
    return False;
  }

  return True;
}

void QuickTimeFileSink
::afterGettingFrame(void* clientData, unsigned packetDataSize,
		    unsigned numTruncatedBytes,
		    struct timeval presentationTime,
		    unsigned /*durationInMicroseconds*/) {
  SubsessionIOState* ioState = (SubsessionIOState*)clientData;
  if (!ioState->syncOK(presentationTime)) {
    // Ignore this data:
    ioState->fOurSink.continuePlaying();
    return;
  }
  if (numTruncatedBytes > 0) {
    ioState->envir() << "QuickTimeFileSink::afterGettingFrame(): The input frame data was too large for our buffer.  "
                     << numTruncatedBytes
                     << " bytes of trailing data was dropped!  Correct this by increasing the \"bufferSize\" parameter in the \"createNew()\" call.\n";
  }
  ioState->afterGettingFrame(packetDataSize, presentationTime);
}

void QuickTimeFileSink::onSourceClosure(void* clientData) {
  SubsessionIOState* ioState = (SubsessionIOState*)clientData;
  ioState->onSourceClosure();
}

void QuickTimeFileSink::onSourceClosure1() {
  // Check whether *all* of the subsession sources have closed.
  // If not, do nothing for now:
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    SubsessionIOState* ioState
      = (SubsessionIOState*)(subsession->miscPtr);
    if (ioState == NULL) continue;

    if (ioState->fOurSourceIsActive) return; // this source hasn't closed
  }

  completeOutputFile();

  // Call our specified 'after' function:
  if (fAfterFunc != NULL) {
    (*fAfterFunc)(fAfterClientData);
  }
}

void QuickTimeFileSink::onRTCPBye(void* clientData) {
  SubsessionIOState* ioState = (SubsessionIOState*)clientData;

  struct timeval timeNow;
  gettimeofday(&timeNow, NULL);
  unsigned secsDiff
    = timeNow.tv_sec - ioState->fOurSink.fStartTime.tv_sec;

  MediaSubsession& subsession = ioState->fOurSubsession;
  ioState->envir() << "Received RTCP \"BYE\" on \""
		   << subsession.mediumName()
		   << "/" << subsession.codecName()
		   << "\" subsession (after "
		   << secsDiff << " seconds)\n";

  // Handle the reception of a RTCP "BYE" as if the source had closed:
  ioState->onSourceClosure();
}

static Boolean timevalGE(struct timeval const& tv1,
			 struct timeval const& tv2) {
  return (unsigned)tv1.tv_sec > (unsigned)tv2.tv_sec
    || (tv1.tv_sec == tv2.tv_sec
	&& (unsigned)tv1.tv_usec >= (unsigned)tv2.tv_usec);
}

void QuickTimeFileSink::completeOutputFile() {
  if (fHaveCompletedOutputFile || fOutFid == NULL) return;

  // Begin by filling in the initial "mdat" atom with the current
  // file size:
  int64_t curFileSize = TellFile64(fOutFid);
  setWord64(fMDATposition, (u_int64_t)curFileSize);

  // Then, note the time of the first received data:
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    SubsessionIOState* ioState
      = (SubsessionIOState*)(subsession->miscPtr);
    if (ioState == NULL) continue;

    ChunkDescriptor* const headChunk = ioState->fHeadChunk;
    if (headChunk != NULL
	&& timevalGE(fFirstDataTime, headChunk->fPresentationTime)) {
      fFirstDataTime = headChunk->fPresentationTime;
    }
  }

  // Then, update the QuickTime-specific state for each active track:
  iter.reset();
  while ((subsession = iter.next()) != NULL) {
    SubsessionIOState* ioState
      = (SubsessionIOState*)(subsession->miscPtr);
    if (ioState == NULL) continue;

    ioState->setFinalQTstate();
    // Do the same for a hint track (if any):
    if (ioState->hasHintTrack()) {
      ioState->fHintTrackForUs->setFinalQTstate();
    }
  }

  if (fGenerateMP4Format) {
    // Begin with a "ftyp" atom:
    addAtom_ftyp();
  }

  // Then, add a "moov" atom for the file metadata:
  addAtom_moov();

  // We're done:
  fHaveCompletedOutputFile = True;
}


////////// SubsessionIOState, ChunkDescriptor implementation ///////////

unsigned SubsessionIOState::fCurrentTrackNumber = 0;

SubsessionIOState::SubsessionIOState(QuickTimeFileSink& sink,
				     MediaSubsession& subsession)
  : fHintTrackForUs(NULL), fTrackHintedByUs(NULL),
    fOurSink(sink), fOurSubsession(subsession),
    fLastPacketRTPSeqNum(0), fHaveBeenSynced(False), fQTTotNumSamples(0), 
    fHeadChunk(NULL), fTailChunk(NULL), fNumChunks(0),
    fHeadSyncFrame(NULL), fTailSyncFrame(NULL) {
  fTrackID = ++fCurrentTrackNumber;

  fBuffer = new SubsessionBuffer(fOurSink.fBufferSize);
  fPrevBuffer = sink.fPacketLossCompensate
    ? new SubsessionBuffer(fOurSink.fBufferSize) : NULL;

  FramedSource* subsessionSource = subsession.readSource();
  fOurSourceIsActive = subsessionSource != NULL;

  fPrevFrameState.presentationTime.tv_sec = 0;
  fPrevFrameState.presentationTime.tv_usec = 0;
  fPrevFrameState.seqNum = 0;
}

SubsessionIOState::~SubsessionIOState() {
  delete fBuffer; delete fPrevBuffer;

  // Delete the list of chunk descriptors:
  ChunkDescriptor* chunk = fHeadChunk;
  while (chunk != NULL) {
    ChunkDescriptor* next = chunk->fNextChunk;
    delete chunk;
    chunk = next;
  }

  // Delete the list of sync frames:
  SyncFrame* syncFrame = fHeadSyncFrame;
  while (syncFrame != NULL) {
    SyncFrame* next = syncFrame->nextSyncFrame;
    delete syncFrame;
    syncFrame = next;
  }
}

Boolean SubsessionIOState::setQTstate() {
  char const* noCodecWarning1 = "Warning: We don't implement a QuickTime ";
  char const* noCodecWarning2 = " Media Data Type for the \"";
  char const* noCodecWarning3 = "\" track, so we'll insert a dummy \"????\" Media Data Atom instead.  A separate, codec-specific editing pass will be needed before this track can be played.\n";

  do {
    fQTEnableTrack = True; // enable this track in the movie by default
    fQTTimeScale = fOurSubsession.rtpTimestampFrequency(); // by default
    fQTTimeUnitsPerSample = 1; // by default
    fQTBytesPerFrame = 0;
        // by default - indicates that the whole packet data is a frame
    fQTSamplesPerFrame = 1; // by default

    // Make sure our subsession's medium is one that we know how to
    // represent in a QuickTime file:
    if (isHintTrack()) {
      // Hint tracks are treated specially
      fQTEnableTrack = False; // hint tracks are marked as inactive
      fQTcomponentSubtype = fourChar('h','i','n','t');
      fQTcomponentName = "hint media handler";
      fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_gmhd;
      fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_rtp;
    } else if (strcmp(fOurSubsession.mediumName(), "audio") == 0) {
      fQTcomponentSubtype = fourChar('s','o','u','n');
      fQTcomponentName = "Apple Sound Media Handler";
      fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_smhd;
      fQTMediaDataAtomCreator
	= &QuickTimeFileSink::addAtom_soundMediaGeneral; // by default
      fQTSoundSampleVersion = 0; // by default

      // Make sure that our subsession's codec is one that we can handle:
      if (strcmp(fOurSubsession.codecName(), "X-QT") == 0 ||
	  strcmp(fOurSubsession.codecName(), "X-QUICKTIME") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_genericMedia;
      } else if (strcmp(fOurSubsession.codecName(), "PCMU") == 0) {
	fQTAudioDataType = "ulaw";
	fQTBytesPerFrame = 1;
      } else if (strcmp(fOurSubsession.codecName(), "GSM") == 0) {
	fQTAudioDataType = "agsm";
	fQTBytesPerFrame = 33;
	fQTSamplesPerFrame = 160;
      } else if (strcmp(fOurSubsession.codecName(), "PCMA") == 0) {
	fQTAudioDataType = "alaw";
	fQTBytesPerFrame = 1;
      } else if (strcmp(fOurSubsession.codecName(), "QCELP") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_Qclp;
	fQTSamplesPerFrame = 160;
      } else if (strcmp(fOurSubsession.codecName(), "MPEG4-GENERIC") == 0 ||
		 strcmp(fOurSubsession.codecName(), "MP4A-LATM") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_mp4a;
	fQTTimeUnitsPerSample = 1024; // QT considers each frame to be a 'sample'
	// The time scale (frequency) comes from the 'config' information.
	// It might be different from the RTP timestamp frequency (e.g., aacPlus).
	unsigned frequencyFromConfig
	  = samplingFrequencyFromAudioSpecificConfig(fOurSubsession.fmtp_config());
	if (frequencyFromConfig != 0) fQTTimeScale = frequencyFromConfig;
      } else {
	envir() << noCodecWarning1 << "Audio" << noCodecWarning2
		<< fOurSubsession.codecName() << noCodecWarning3;
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_dummy;
	fQTEnableTrack = False; // disable this track in the movie
      }
    } else if (strcmp(fOurSubsession.mediumName(), "video") == 0) {
      fQTcomponentSubtype = fourChar('v','i','d','e');
      fQTcomponentName = "Apple Video Media Handler";
      fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_vmhd;

      // Make sure that our subsession's codec is one that we can handle:
      if (strcmp(fOurSubsession.codecName(), "X-QT") == 0 ||
	  strcmp(fOurSubsession.codecName(), "X-QUICKTIME") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_genericMedia;
      } else if (strcmp(fOurSubsession.codecName(), "H263-1998") == 0 ||
		 strcmp(fOurSubsession.codecName(), "H263-2000") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_h263;
	fQTTimeScale = 600;
	fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS;
      } else if (strcmp(fOurSubsession.codecName(), "H264") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_avc1;
	fQTTimeScale = 600;
	fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS;
      } else if (strcmp(fOurSubsession.codecName(), "MP4V-ES") == 0) {
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_mp4v;
	fQTTimeScale = 600;
	fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS;
      } else {
	envir() << noCodecWarning1 << "Video" << noCodecWarning2
		<< fOurSubsession.codecName() << noCodecWarning3;
	fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_dummy;
	fQTEnableTrack = False; // disable this track in the movie
      }
    } else {
      envir() << "Warning: We don't implement a QuickTime Media Handler for media type \""
	      << fOurSubsession.mediumName() << "\"";
      break;
    }

#ifdef QT_SUPPORT_PARTIALLY_ONLY
    envir() << "Warning: We don't have sufficient codec-specific information (e.g., sample sizes) to fully generate the \""
	    << fOurSubsession.mediumName() << "/" << fOurSubsession.codecName()
	    << "\" track, so we'll disable this track in the movie.  A separate, codec-specific editing pass will be needed before this track can be played\n";
    fQTEnableTrack = False; // disable this track in the movie
#endif

    return True;
  } while (0);

  envir() << ", so a track for the \"" << fOurSubsession.mediumName()
	  << "/" << fOurSubsession.codecName()
	  << "\" subsession will not be included in the output QuickTime file\n";
  return False;
}

void SubsessionIOState::setFinalQTstate() {
  // Compute derived parameters, by running through the list of chunks:
  fQTDurationT = 0;

  ChunkDescriptor* chunk = fHeadChunk;
  while (chunk != NULL) {
    unsigned const numFrames = chunk->fNumFrames;
    unsigned const dur = numFrames*chunk->fFrameDuration;
    fQTDurationT += dur;

    chunk = chunk->fNextChunk;
  }

  // Convert this duration from track to movie time scale:
  double scaleFactor = fOurSink.movieTimeScale()/(double)fQTTimeScale;
  fQTDurationM = (unsigned)(fQTDurationT*scaleFactor);

  if (fQTDurationM > fOurSink.fMaxTrackDurationM) {
    fOurSink.fMaxTrackDurationM = fQTDurationM;
  }
}

void SubsessionIOState::afterGettingFrame(unsigned packetDataSize,
					  struct timeval presentationTime) {
  // Begin by checking whether there was a gap in the RTP stream.
  // If so, try to compensate for this (if desired):
  if (fOurSubsession.rtpSource() != NULL) { // we have a RTP stream
    unsigned short rtpSeqNum
      = fOurSubsession.rtpSource()->curPacketRTPSeqNum();
    if (fOurSink.fPacketLossCompensate && fPrevBuffer->bytesInUse() > 0) {
      short seqNumGap = rtpSeqNum - fLastPacketRTPSeqNum;
      for (short i = 1; i < seqNumGap; ++i) {
	// Insert a copy of the previous frame, to compensate for the loss:
	useFrame(*fPrevBuffer);
      }
    }
    fLastPacketRTPSeqNum = rtpSeqNum;
  }

  // Now, continue working with the frame that we just got
  fOurSink.noteRecordedFrame(fOurSubsession, packetDataSize, presentationTime);

  if (fBuffer->bytesInUse() == 0) {
    fBuffer->setPresentationTime(presentationTime);
  }
  fBuffer->addBytes(packetDataSize);

  // If our RTP source is a "QuickTimeGenericRTPSource", then
  // use its 'qtState' to set some parameters that we need:
  if (fOurSubsession.rtpSource() != NULL // we have a RTP stream
      && fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_genericMedia) {
    QuickTimeGenericRTPSource* rtpSource
      = (QuickTimeGenericRTPSource*)fOurSubsession.rtpSource();
    QuickTimeGenericRTPSource::QTState& qtState = rtpSource->qtState;
    fQTTimeScale = qtState.timescale;
    if (qtState.width != 0) {
      fOurSink.fMovieWidth = qtState.width;
    }
    if (qtState.height != 0) {
      fOurSink.fMovieHeight = qtState.height;
    }

    // Also, if the media type in the "sdAtom" is one that we recognize
    // to have a special parameters, then fix this here:
    if (qtState.sdAtomSize >= 8) {
      char const* atom = qtState.sdAtom;
      unsigned mediaType = fourChar(atom[4],atom[5],atom[6],atom[7]);
      switch (mediaType) {
      case fourChar('a','g','s','m'): {
	fQTBytesPerFrame = 33;
	fQTSamplesPerFrame = 160;
	break;
      }
      case fourChar('Q','c','l','p'): {
	fQTBytesPerFrame = 35;
	fQTSamplesPerFrame = 160;
	break;
      }
      case fourChar('H','c','l','p'): {
	fQTBytesPerFrame = 17;
	fQTSamplesPerFrame = 160;
	break;
      }
      case fourChar('h','2','6','3'): {
	fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS;
	break;
      }
      }
    }
  } else if (fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_Qclp) {
    // For QCELP data, make a note of the frame size (even though it's the
    // same as the packet data size), because it varies depending on the
    // 'rate' of the stream, and this size gets used later when setting up
    // the 'Qclp' QuickTime atom:
    fQTBytesPerFrame = packetDataSize;
  }

  useFrame(*fBuffer);
  if (fOurSink.fPacketLossCompensate) {
    // Save this frame, in case we need it for recovery:
    SubsessionBuffer* tmp = fPrevBuffer; // assert: != NULL
    fPrevBuffer = fBuffer;
    fBuffer = tmp;
  }
  fBuffer->reset(); // for the next input

  // Now, try getting more frames:
  fOurSink.continuePlaying();
}

void SubsessionIOState::useFrame(SubsessionBuffer& buffer) {
  unsigned char* const frameSource = buffer.dataStart();
  unsigned const frameSize = buffer.bytesInUse();
  struct timeval const& presentationTime = buffer.presentationTime();
  int64_t const destFileOffset = TellFile64(fOurSink.fOutFid);
  unsigned sampleNumberOfFrameStart = fQTTotNumSamples + 1;
  Boolean avcHack = fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_avc1;

  // If we're not syncing streams, or this subsession is not video, then
  // just give this frame a fixed duration:
  if (!fOurSink.fSyncStreams
      || fQTcomponentSubtype != fourChar('v','i','d','e')) {
    unsigned const frameDuration = fQTTimeUnitsPerSample*fQTSamplesPerFrame;
    unsigned frameSizeToUse = frameSize;
    if (avcHack) frameSizeToUse += 4; // H.264/AVC gets the frame size prefix

    fQTTotNumSamples += useFrame1(frameSizeToUse, presentationTime, frameDuration, destFileOffset);
  } else {
    // For synced video streams, we use the difference between successive
    // frames' presentation times as the 'frame duration'.  So, record
    // information about the *previous* frame:
    struct timeval const& ppt = fPrevFrameState.presentationTime; //abbrev
    if (ppt.tv_sec != 0 || ppt.tv_usec != 0) {
      // There has been a previous frame.
      double duration = (presentationTime.tv_sec - ppt.tv_sec)
	+ (presentationTime.tv_usec - ppt.tv_usec)/1000000.0;
      if (duration < 0.0) duration = 0.0;
      unsigned frameDuration
	= (unsigned)((2*duration*fQTTimeScale+1)/2); // round
      unsigned frameSizeToUse = fPrevFrameState.frameSize;
      if (avcHack) frameSizeToUse += 4; // H.264/AVC gets the frame size prefix

      unsigned numSamples
	= useFrame1(frameSizeToUse, ppt, frameDuration, fPrevFrameState.destFileOffset);
      fQTTotNumSamples += numSamples;
      sampleNumberOfFrameStart = fQTTotNumSamples + 1;
    }

    if (avcHack && (*frameSource == H264_IDR_FRAME)) {
      SyncFrame* newSyncFrame = new SyncFrame(fQTTotNumSamples + 1);
      if (fTailSyncFrame == NULL) {
        fHeadSyncFrame = newSyncFrame;
      } else {
        fTailSyncFrame->nextSyncFrame = newSyncFrame;
      }
      fTailSyncFrame = newSyncFrame;
    }

    // Remember the current frame for next time:
    fPrevFrameState.frameSize = frameSize;
    fPrevFrameState.presentationTime = presentationTime;
    fPrevFrameState.destFileOffset = destFileOffset;
  }

  if (avcHack) fOurSink.addWord(frameSize);

  // Write the data into the file:
  fwrite(frameSource, 1, frameSize, fOurSink.fOutFid);

  // If we have a hint track, then write to it also (only if we have a RTP stream):
  if (hasHintTrack() && fOurSubsession.rtpSource() != NULL) {
    // Because presentation times are used for RTP packet timestamps,
    // we don't starting writing to the hint track until we've been synced:
    if (!fHaveBeenSynced) {
      fHaveBeenSynced = fOurSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP();
    }
    if (fHaveBeenSynced) {
      fHintTrackForUs->useFrameForHinting(frameSize, presentationTime,
					  sampleNumberOfFrameStart);
    }
  }
}

void SubsessionIOState::useFrameForHinting(unsigned frameSize,
					   struct timeval presentationTime,
					   unsigned startSampleNumber) {
  // At this point, we have a single, combined frame - not individual packets.
  // For the hint track, we need to split the frame back up into separate packets.
  // However, for some RTP sources, then we also need to reuse the special
  // header bytes that were at the start of each of the RTP packets.
  Boolean hack263 = strcmp(fOurSubsession.codecName(), "H263-1998") == 0;
  Boolean hackm4a_generic = strcmp(fOurSubsession.mediumName(), "audio") == 0
    && strcmp(fOurSubsession.codecName(), "MPEG4-GENERIC") == 0;
  Boolean hackm4a_latm = strcmp(fOurSubsession.mediumName(), "audio") == 0
    && strcmp(fOurSubsession.codecName(), "MP4A-LATM") == 0;
  Boolean hackm4a = hackm4a_generic || hackm4a_latm;
  Boolean haveSpecialHeaders = (hack263 || hackm4a_generic);

  // If there has been a previous frame, then output a 'hint sample' for it.
  // (We use the current frame's presentation time to compute the previous
  // hint sample's duration.)
  RTPSource* const rs = fOurSubsession.rtpSource(); // abbrev (ASSERT: != NULL)
  struct timeval const& ppt = fPrevFrameState.presentationTime; //abbrev
  if (ppt.tv_sec != 0 || ppt.tv_usec != 0) {
    double duration = (presentationTime.tv_sec - ppt.tv_sec)
      + (presentationTime.tv_usec - ppt.tv_usec)/1000000.0;
    if (duration < 0.0) duration = 0.0;
    unsigned msDuration = (unsigned)(duration*1000); // milliseconds
    if (msDuration > fHINF.dmax) fHINF.dmax = msDuration;
    unsigned hintSampleDuration
      = (unsigned)((2*duration*fQTTimeScale+1)/2); // round
    if (hackm4a) {
      // Because multiple AAC frames can appear in a RTP packet, the presentation
      // times of the second and subsequent frames will not be accurate.
      // So, use the known "hintSampleDuration" instead:
      hintSampleDuration = fTrackHintedByUs->fQTTimeUnitsPerSample;

      // Also, if the 'time scale' was different from the RTP timestamp frequency,
      // (as can happen with aacPlus), then we need to scale "hintSampleDuration"
      // accordingly:
      if (fTrackHintedByUs->fQTTimeScale != fOurSubsession.rtpTimestampFrequency()) {
	unsigned const scalingFactor
	  = fOurSubsession.rtpTimestampFrequency()/fTrackHintedByUs->fQTTimeScale ;
	hintSampleDuration *= scalingFactor;
      }
    }

    int64_t const hintSampleDestFileOffset = TellFile64(fOurSink.fOutFid);

    unsigned const maxPacketSize = 1450;
    unsigned short numPTEntries
      = (fPrevFrameState.frameSize + (maxPacketSize-1))/maxPacketSize; // normal case
    unsigned char* immediateDataPtr = NULL;
    unsigned immediateDataBytesRemaining = 0;
    if (haveSpecialHeaders) { // special case
      numPTEntries = fPrevFrameState.numSpecialHeaders;
      immediateDataPtr = fPrevFrameState.specialHeaderBytes;
      immediateDataBytesRemaining
	= fPrevFrameState.specialHeaderBytesLength;
    }
    unsigned hintSampleSize
      = fOurSink.addHalfWord(numPTEntries);// Entry count
    hintSampleSize += fOurSink.addHalfWord(0x0000); // Reserved

    unsigned offsetWithinSample = 0;
    for (unsigned i = 0; i < numPTEntries; ++i) {
      // Output a Packet Table entry (representing a single RTP packet):
      unsigned short numDTEntries = 1;
      unsigned short seqNum = fPrevFrameState.seqNum++;
          // Note: This assumes that the input stream had no packets lost #####
      unsigned rtpHeader = fPrevFrameState.rtpHeader;
      if (i+1 < numPTEntries) {
	// This is not the last RTP packet, so clear the marker bit:
	rtpHeader &=~ (1<<23);
      }
      unsigned dataFrameSize = (i+1 < numPTEntries)
	? maxPacketSize : fPrevFrameState.frameSize - i*maxPacketSize; // normal case
      unsigned sampleNumber = fPrevFrameState.startSampleNumber;

      unsigned char immediateDataLen = 0;
      if (haveSpecialHeaders) { // special case
	++numDTEntries; // to include a Data Table entry for the special hdr
	if (immediateDataBytesRemaining > 0) {
	  if (hack263) {
	    immediateDataLen = *immediateDataPtr++;
	    --immediateDataBytesRemaining;
	    if (immediateDataLen > immediateDataBytesRemaining) {
	      // shouldn't happen (length byte was bad)
	      immediateDataLen = immediateDataBytesRemaining;
	    }
	  } else {
	    immediateDataLen = fPrevFrameState.specialHeaderBytesLength;
	  }
	}
	dataFrameSize = fPrevFrameState.packetSizes[i] - immediateDataLen;

	if (hack263) {
	  Boolean PbitSet
	    = immediateDataLen >= 1 && (immediateDataPtr[0]&0x4) != 0;
	  if (PbitSet) {
	    offsetWithinSample += 2; // to omit the two leading 0 bytes
	  }
	}
      }

      // Output the Packet Table:
      hintSampleSize += fOurSink.addWord(0); // Relative transmission time
      hintSampleSize += fOurSink.addWord(rtpHeader|seqNum);
          // RTP header info + RTP sequence number
      hintSampleSize += fOurSink.addHalfWord(0x0000); // Flags
      hintSampleSize += fOurSink.addHalfWord(numDTEntries); // Entry count
      unsigned totalPacketSize = 0;

      // Output the Data Table:
      if (haveSpecialHeaders) {
	//   use the "Immediate Data" format (1):
	hintSampleSize += fOurSink.addByte(1); // Source
	unsigned char len = immediateDataLen > 14 ? 14 : immediateDataLen;
	hintSampleSize += fOurSink.addByte(len); // Length
	totalPacketSize += len; fHINF.dimm += len;
	unsigned char j;
	for (j = 0; j < len; ++j) {
	  hintSampleSize += fOurSink.addByte(immediateDataPtr[j]); // Data
	}
	for (j = len; j < 14; ++j) {
	  hintSampleSize += fOurSink.addByte(0); // Data (padding)
	}

	immediateDataPtr += immediateDataLen;
	immediateDataBytesRemaining -= immediateDataLen;
      }
      //   use the "Sample Data" format (2):
      hintSampleSize += fOurSink.addByte(2); // Source
      hintSampleSize += fOurSink.addByte(0); // Track ref index
      hintSampleSize += fOurSink.addHalfWord(dataFrameSize); // Length
      totalPacketSize += dataFrameSize; fHINF.dmed += dataFrameSize;
      hintSampleSize += fOurSink.addWord(sampleNumber); // Sample number
      hintSampleSize += fOurSink.addWord(offsetWithinSample); // Offset
      // Get "bytes|samples per compression block" from the hinted track:
      unsigned short const bytesPerCompressionBlock
	= fTrackHintedByUs->fQTBytesPerFrame;
      unsigned short const samplesPerCompressionBlock
	= fTrackHintedByUs->fQTSamplesPerFrame;
      hintSampleSize += fOurSink.addHalfWord(bytesPerCompressionBlock);
      hintSampleSize += fOurSink.addHalfWord(samplesPerCompressionBlock);

      offsetWithinSample += dataFrameSize;// for the next iteration (if any)

      // Tally statistics for this packet:
      fHINF.nump += 1;
      fHINF.tpyl += totalPacketSize;
      totalPacketSize += 12; // add in the size of the RTP header
      fHINF.trpy += totalPacketSize;
      if (totalPacketSize > fHINF.pmax) fHINF.pmax = totalPacketSize;
    }

    // Make note of this completed hint sample frame:
    fQTTotNumSamples += useFrame1(hintSampleSize, ppt, hintSampleDuration,
				  hintSampleDestFileOffset);
  }

  // Remember this frame for next time:
  fPrevFrameState.frameSize = frameSize;
  fPrevFrameState.presentationTime = presentationTime;
  fPrevFrameState.startSampleNumber = startSampleNumber;
  fPrevFrameState.rtpHeader
    = rs->curPacketMarkerBit()<<23
    | (rs->rtpPayloadFormat()&0x7F)<<16;
  if (hack263) {
    H263plusVideoRTPSource* rs_263 = (H263plusVideoRTPSource*)rs;
    fPrevFrameState.numSpecialHeaders = rs_263->fNumSpecialHeaders;
    fPrevFrameState.specialHeaderBytesLength = rs_263->fSpecialHeaderBytesLength;
    unsigned i;
    for (i = 0; i < rs_263->fSpecialHeaderBytesLength; ++i) {
      fPrevFrameState.specialHeaderBytes[i] = rs_263->fSpecialHeaderBytes[i];
    }
    for (i = 0; i < rs_263->fNumSpecialHeaders; ++i) {
      fPrevFrameState.packetSizes[i] = rs_263->fPacketSizes[i];
    }
  } else if (hackm4a_generic) {
    // Synthesize a special header, so that this frame can be in its own RTP packet.
    unsigned const sizeLength = fOurSubsession.attrVal_unsigned("sizelength");
    unsigned const indexLength = fOurSubsession.attrVal_unsigned("indexlength");
    if (sizeLength + indexLength != 16) {
      envir() << "Warning: unexpected 'sizeLength' " << sizeLength
	      << " and 'indexLength' " << indexLength
	      << "seen when creating hint track\n";
    }
    fPrevFrameState.numSpecialHeaders = 1;
    fPrevFrameState.specialHeaderBytesLength = 4;
    fPrevFrameState.specialHeaderBytes[0] = 0; // AU_headers_length (high byte)
    fPrevFrameState.specialHeaderBytes[1] = 16; // AU_headers_length (low byte)
    fPrevFrameState.specialHeaderBytes[2] = ((frameSize<<indexLength)&0xFF00)>>8;
    fPrevFrameState.specialHeaderBytes[3] = (frameSize<<indexLength);
    fPrevFrameState.packetSizes[0]
      = fPrevFrameState.specialHeaderBytesLength + frameSize;
  }
}

unsigned SubsessionIOState::useFrame1(unsigned sourceDataSize,
				      struct timeval presentationTime,
				      unsigned frameDuration,
				      int64_t destFileOffset) {
  // Figure out the actual frame size for this data:
  unsigned frameSize = fQTBytesPerFrame;
  if (frameSize == 0) {
    // The entire packet data is assumed to be a frame:
    frameSize = sourceDataSize;
  }
  unsigned const numFrames = sourceDataSize/frameSize;
  unsigned const numSamples = numFrames*fQTSamplesPerFrame;

  // Record the information about which 'chunk' this data belongs to:
  ChunkDescriptor* newTailChunk;
  if (fTailChunk == NULL) {
    newTailChunk = fHeadChunk
      = new ChunkDescriptor(destFileOffset, sourceDataSize,
			    frameSize, frameDuration, presentationTime);
  } else {
    newTailChunk = fTailChunk->extendChunk(destFileOffset, sourceDataSize,
					   frameSize, frameDuration,
					   presentationTime);
  }
  if (newTailChunk != fTailChunk) {
   // This data created a new chunk, rather than extending the old one
    ++fNumChunks;
    fTailChunk = newTailChunk;
  }

  return numSamples;
}

void SubsessionIOState::onSourceClosure() {
  fOurSourceIsActive = False;
  fOurSink.onSourceClosure1();
}

Boolean SubsessionIOState::syncOK(struct timeval presentationTime) {
  QuickTimeFileSink& s = fOurSink; // abbreviation
  if (!s.fSyncStreams || fOurSubsession.rtpSource() == NULL) return True; // we don't care

  if (s.fNumSyncedSubsessions < s.fNumSubsessions) {
    // Not all subsessions have yet been synced.  Check whether ours was
    // one of the unsynced ones, and, if so, whether it is now synced:
    if (!fHaveBeenSynced) {
      // We weren't synchronized before
      if (fOurSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP()) {
	// H264 ?
	if (fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_avc1) {
	  // special case: audio + H264 video: wait until audio is in sync
	  if ((s.fNumSubsessions == 2) && (s.fNumSyncedSubsessions < (s.fNumSubsessions - 1))) return False;

	  // if audio is in sync, wait for the next IDR frame to start
	  unsigned char* const frameSource = fBuffer->dataStart();
	  if (*frameSource != H264_IDR_FRAME) return False;
	}
	// But now we are
	fHaveBeenSynced = True;
	fSyncTime = presentationTime;
	++s.fNumSyncedSubsessions;

	if (timevalGE(fSyncTime, s.fNewestSyncTime)) {
	  s.fNewestSyncTime = fSyncTime;
	}
      }
    }
  }

  // Check again whether all subsessions have been synced:
  if (s.fNumSyncedSubsessions < s.fNumSubsessions) return False;

  // Allow this data if it is more recent than the newest sync time:
  return timevalGE(presentationTime, s.fNewestSyncTime);
}

void SubsessionIOState::setHintTrack(SubsessionIOState* hintedTrack,
				     SubsessionIOState* hintTrack) {
  if (hintedTrack != NULL) hintedTrack->fHintTrackForUs = hintTrack;
  if (hintTrack != NULL) hintTrack->fTrackHintedByUs = hintedTrack;
}

SyncFrame::SyncFrame(unsigned frameNum)
  : nextSyncFrame(NULL), sfFrameNum(frameNum) {
}  

void Count64::operator+=(unsigned arg) {
  unsigned newLo = lo + arg;
  if (newLo < lo) { // lo has overflowed
    ++hi;
  }
  lo = newLo;
}

ChunkDescriptor
::ChunkDescriptor(int64_t offsetInFile, unsigned size,
		  unsigned frameSize, unsigned frameDuration,
		  struct timeval presentationTime)
  : fNextChunk(NULL), fOffsetInFile(offsetInFile),
    fNumFrames(size/frameSize),
    fFrameSize(frameSize), fFrameDuration(frameDuration),
    fPresentationTime(presentationTime) {
}

ChunkDescriptor* ChunkDescriptor
::extendChunk(int64_t newOffsetInFile, unsigned newSize,
	      unsigned newFrameSize, unsigned newFrameDuration,
	      struct timeval newPresentationTime) {
  // First, check whether the new space is just at the end of this
  // existing chunk:
  if (newOffsetInFile == fOffsetInFile + fNumFrames*fFrameSize) {
    // We can extend this existing chunk, provided that the frame size
    // and frame duration have not changed:
    if (newFrameSize == fFrameSize && newFrameDuration == fFrameDuration) {
      fNumFrames += newSize/fFrameSize;
      return this;
    }
  }

  // We'll allocate a new ChunkDescriptor, and link it to the end of us:
  ChunkDescriptor* newDescriptor
    = new ChunkDescriptor(newOffsetInFile, newSize,
			  newFrameSize, newFrameDuration,
			  newPresentationTime);

  fNextChunk = newDescriptor;

  return newDescriptor;
}


////////// QuickTime-specific implementation //////////

unsigned QuickTimeFileSink::addWord64(u_int64_t word) {
  addByte((unsigned char)(word>>56)); addByte((unsigned char)(word>>48));
  addByte((unsigned char)(word>>40)); addByte((unsigned char)(word>>32));
  addByte((unsigned char)(word>>24)); addByte((unsigned char)(word>>16));
  addByte((unsigned char)(word>>8)); addByte((unsigned char)(word));

  return 8;
}

unsigned QuickTimeFileSink::addWord(unsigned word) {
  addByte(word>>24); addByte(word>>16);
  addByte(word>>8); addByte(word);

  return 4;
}

unsigned QuickTimeFileSink::addHalfWord(unsigned short halfWord) {
  addByte((unsigned char)(halfWord>>8)); addByte((unsigned char)halfWord);

  return 2;
}

unsigned QuickTimeFileSink::addZeroWords(unsigned numWords) {
  for (unsigned i = 0; i < numWords; ++i) {
    addWord(0);
  }

  return numWords*4;
}

unsigned QuickTimeFileSink::add4ByteString(char const* str) {
  addByte(str[0]); addByte(str[1]); addByte(str[2]); addByte(str[3]);

  return 4;
}

unsigned QuickTimeFileSink::addArbitraryString(char const* str,
					       Boolean oneByteLength) {
  unsigned size = 0;
  if (oneByteLength) {
    // Begin with a byte containing the string length:
    unsigned strLength = strlen(str);
    if (strLength >= 256) {
      envir() << "QuickTimeFileSink::addArbitraryString(\""
	      << str << "\") saw string longer than we know how to handle ("
	      << strLength << "\n";
    }
    size += addByte((unsigned char)strLength);
  }

  while (*str != '\0') {
    size += addByte(*str++);
  }

  return size;
}

unsigned QuickTimeFileSink::addAtomHeader(char const* atomName) {
  // Output a placeholder for the 4-byte size:
  addWord(0);

  // Output the 4-byte atom name:
  add4ByteString(atomName);

  return 8;
}

unsigned QuickTimeFileSink::addAtomHeader64(char const* atomName) {
  // Output 64Bit size marker
  addWord(1);

  // Output the 4-byte atom name:
  add4ByteString(atomName);

  addWord64(0);

  return 16;
}

void QuickTimeFileSink::setWord(int64_t filePosn, unsigned size) {
  do {
    if (SeekFile64(fOutFid, filePosn, SEEK_SET) < 0) break;
    addWord(size);
    if (SeekFile64(fOutFid, 0, SEEK_END) < 0) break; // go back to where we were

    return;
  } while (0);

  // One of the SeekFile64()s failed, probable because we're not a seekable file
  envir() << "QuickTimeFileSink::setWord(): SeekFile64 failed (err "
	  << envir().getErrno() << ")\n";
}

void QuickTimeFileSink::setWord64(int64_t filePosn, u_int64_t size) {
  do {
    if (SeekFile64(fOutFid, filePosn, SEEK_SET) < 0) break;
    addWord64(size);
    if (SeekFile64(fOutFid, 0, SEEK_END) < 0) break; // go back to where we were

    return;
  } while (0);

  // One of the SeekFile64()s failed, probable because we're not a seekable file
  envir() << "QuickTimeFileSink::setWord64(): SeekFile64 failed (err "
	  << envir().getErrno() << ")\n";
}

// Methods for writing particular atoms.  Note the following macros:

#define addAtom(name) \
    unsigned QuickTimeFileSink::addAtom_##name() { \
    int64_t initFilePosn = TellFile64(fOutFid); \
    unsigned size = addAtomHeader("" #name "")

#define addAtomEnd \
  setWord(initFilePosn, size); \
  return size; \
}

addAtom(ftyp);
  size += add4ByteString("mp42");
  size += addWord(0x00000000);
  size += add4ByteString("mp42");
  size += add4ByteString("isom");
addAtomEnd;

addAtom(moov);
  size += addAtom_mvhd();

  if (fGenerateMP4Format) {
    size += addAtom_iods();
  }

  // Add a 'trak' atom for each subsession:
  // (For some unknown reason, QuickTime Player (5.0 at least)
  //  doesn't display the movie correctly unless the audio track
  //  (if present) appears before the video track.  So ensure this here.)
  MediaSubsessionIterator iter(fInputSession);
  MediaSubsession* subsession;
  while ((subsession = iter.next()) != NULL) {
    fCurrentIOState = (SubsessionIOState*)(subsession->miscPtr);
    if (fCurrentIOState == NULL) continue;
    if (strcmp(subsession->mediumName(), "audio") != 0) continue;

    size += addAtom_trak();

    if (fCurrentIOState->hasHintTrack()) {
      // This track has a hint track; output it also:
      fCurrentIOState = fCurrentIOState->fHintTrackForUs;
      size += addAtom_trak();
    }
  }
  iter.reset();
  while ((subsession = iter.next()) != NULL) {
    fCurrentIOState = (SubsessionIOState*)(subsession->miscPtr);
    if (fCurrentIOState == NULL) continue;
    if (strcmp(subsession->mediumName(), "audio") == 0) continue;

    size += addAtom_trak();

    if (fCurrentIOState->hasHintTrack()) {
      // This track has a hint track; output it also:
      fCurrentIOState = fCurrentIOState->fHintTrackForUs;
      size += addAtom_trak();
    }
  }
addAtomEnd;

addAtom(mvhd);
  size += addWord(0x00000000); // Version + Flags
  size += addWord(fAppleCreationTime); // Creation time
  size += addWord(fAppleCreationTime); // Modification time

  // For the "Time scale" field, use the largest RTP timestamp frequency
  // that we saw in any of the subsessions.
  size += addWord(movieTimeScale()); // Time scale

  unsigned const duration = fMaxTrackDurationM;
  fMVHD_durationPosn = TellFile64(fOutFid);
  size += addWord(duration); // Duration

  size += addWord(0x00010000); // Preferred rate
  size += addWord(0x01000000); // Preferred volume + Reserved[0]
  size += addZeroWords(2); // Reserved[1-2]
  size += addWord(0x00010000); // matrix top left corner
  size += addZeroWords(3); // matrix
  size += addWord(0x00010000); // matrix center
  size += addZeroWords(3); // matrix
  size += addWord(0x40000000); // matrix bottom right corner
  size += addZeroWords(6); // various time fields
  size += addWord(SubsessionIOState::fCurrentTrackNumber+1);// Next track ID
addAtomEnd;

addAtom(iods);
  size += addWord(0x00000000); // Version + Flags
  size += addWord(0x10808080);
  size += addWord(0x07004FFF);
  size += addWord(0xFF0FFFFF);
addAtomEnd;

addAtom(trak);
  size += addAtom_tkhd();

  // If we're synchronizing the media streams (or are a hint track),
  // add an edit list that helps do this:
  if (fCurrentIOState->fHeadChunk != NULL
      && (fSyncStreams || fCurrentIOState->isHintTrack())) {
    size += addAtom_edts();
  }

  // If we're generating a hint track, add a 'tref' atom:
  if (fCurrentIOState->isHintTrack()) size += addAtom_tref();

  size += addAtom_mdia();

  // If we're generating a hint track, add a 'udta' atom:
  if (fCurrentIOState->isHintTrack()) size += addAtom_udta();
addAtomEnd;

addAtom(tkhd);
  if (fCurrentIOState->fQTEnableTrack) {
    size += addWord(0x0000000F); // Version +  Flags
  } else {
    // Disable this track in the movie:
    size += addWord(0x00000000); // Version +  Flags
  }
  size += addWord(fAppleCreationTime); // Creation time
  size += addWord(fAppleCreationTime); // Modification time
  size += addWord(fCurrentIOState->fTrackID); // Track ID
  size += addWord(0x00000000); // Reserved

  unsigned const duration = fCurrentIOState->fQTDurationM; // movie units
  fCurrentIOState->fTKHD_durationPosn = TellFile64(fOutFid);
  size += addWord(duration); // Duration
  size += addZeroWords(3); // Reserved+Layer+Alternate grp
  size += addWord(0x01000000); // Volume + Reserved
  size += addWord(0x00010000); // matrix top left corner
  size += addZeroWords(3); // matrix
  size += addWord(0x00010000); // matrix center
  size += addZeroWords(3); // matrix
  size += addWord(0x40000000); // matrix bottom right corner
  if (strcmp(fCurrentIOState->fOurSubsession.mediumName(), "video") == 0) {
    size += addWord(fMovieWidth<<16); // Track width
    size += addWord(fMovieHeight<<16); // Track height
  } else {
    size += addZeroWords(2); // not video: leave width and height fields zero
  }
addAtomEnd;

addAtom(edts);
  size += addAtom_elst();
addAtomEnd;

#define addEdit1(duration,trackPosition) do { \
      unsigned trackDuration \
        = (unsigned) ((2*(duration)*movieTimeScale()+1)/2); \
            /* in movie time units */ \
      size += addWord(trackDuration); /* Track duration */ \
      totalDurationOfEdits += trackDuration; \
      size += addWord(trackPosition); /* Media time */ \
      size += addWord(0x00010000); /* Media rate (1x) */ \
      ++numEdits; \
} while (0)
#define addEdit(duration) addEdit1((duration),editTrackPosition)
#define addEmptyEdit(duration) addEdit1((duration),(~0))

addAtom(elst);
  size += addWord(0x00000000); // Version + Flags

  // Add a dummy "Number of entries" field
  // (and remember its position).  We'll fill this field in later:
  int64_t numEntriesPosition = TellFile64(fOutFid);
  size += addWord(0); // dummy for "Number of entries"
  unsigned numEdits = 0;
  unsigned totalDurationOfEdits = 0; // in movie time units

  // Run through our chunks, looking at their presentation times.
  // From these, figure out the edits that need to be made to keep
  // the track media data in sync with the presentation times.

  double const syncThreshold = 0.1; // 100 ms
    // don't allow the track to get out of sync by more than this

  struct timeval editStartTime = fFirstDataTime;
  unsigned editTrackPosition = 0;
  unsigned currentTrackPosition = 0;
  double trackDurationOfEdit = 0.0;
  unsigned chunkDuration = 0;

  ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
  while (chunk != NULL) {
    struct timeval const& chunkStartTime = chunk->fPresentationTime;
    double movieDurationOfEdit
      = (chunkStartTime.tv_sec - editStartTime.tv_sec)
      + (chunkStartTime.tv_usec - editStartTime.tv_usec)/1000000.0;
    trackDurationOfEdit = (currentTrackPosition-editTrackPosition)
      / (double)(fCurrentIOState->fQTTimeScale);

    double outOfSync = movieDurationOfEdit - trackDurationOfEdit;

    if (outOfSync > syncThreshold) {
      // The track's data is too short, so end this edit, add a new
      // 'empty' edit after it, and start a new edit
      // (at the current track posn.):
      if (trackDurationOfEdit > 0.0) addEdit(trackDurationOfEdit);
      addEmptyEdit(outOfSync);

      editStartTime = chunkStartTime;
      editTrackPosition = currentTrackPosition;
    } else if (outOfSync < -syncThreshold) {
      // The track's data is too long, so end this edit, and start
      // a new edit (pointing at the current track posn.):
      if (movieDurationOfEdit > 0.0) addEdit(movieDurationOfEdit);

      editStartTime = chunkStartTime;
      editTrackPosition = currentTrackPosition;
    }

    // Note the duration of this chunk:
    unsigned numChannels = fCurrentIOState->fOurSubsession.numChannels();
    chunkDuration = chunk->fNumFrames*chunk->fFrameDuration/numChannels;
    currentTrackPosition += chunkDuration;

    chunk = chunk->fNextChunk;
  }

  // Write out the final edit
  trackDurationOfEdit
      += (double)chunkDuration/fCurrentIOState->fQTTimeScale;
  if (trackDurationOfEdit > 0.0) addEdit(trackDurationOfEdit);

  // Now go back and fill in the "Number of entries" field:
  setWord(numEntriesPosition, numEdits);

  // Also, if the sum of all of the edit durations exceeds the
  // track duration that we already computed (from sample durations),
  // then reset the track duration to this new value:
  if (totalDurationOfEdits > fCurrentIOState->fQTDurationM) {
    fCurrentIOState->fQTDurationM = totalDurationOfEdits;
    setWord(fCurrentIOState->fTKHD_durationPosn, totalDurationOfEdits);

    // Also, check whether the overall movie duration needs to change:
    if (totalDurationOfEdits > fMaxTrackDurationM) {
      fMaxTrackDurationM = totalDurationOfEdits;
      setWord(fMVHD_durationPosn, totalDurationOfEdits);
    }

    // Also, convert to track time scale:
    double scaleFactor
      = fCurrentIOState->fQTTimeScale/(double)movieTimeScale();
    fCurrentIOState->fQTDurationT
      = (unsigned)(totalDurationOfEdits*scaleFactor);
  }
addAtomEnd;

addAtom(tref);
  size += addAtom_hint();
addAtomEnd;

addAtom(hint);
  SubsessionIOState* hintedTrack = fCurrentIOState->fTrackHintedByUs;
    // Assert: hintedTrack != NULL
  size += addWord(hintedTrack->fTrackID);
addAtomEnd;

addAtom(mdia);
  size += addAtom_mdhd();
  size += addAtom_hdlr();
  size += addAtom_minf();
addAtomEnd;

addAtom(mdhd);
  size += addWord(0x00000000); // Version + Flags
  size += addWord(fAppleCreationTime); // Creation time
  size += addWord(fAppleCreationTime); // Modification time

  unsigned const timeScale = fCurrentIOState->fQTTimeScale;
  size += addWord(timeScale); // Time scale

  unsigned const duration = fCurrentIOState->fQTDurationT; // track units
  size += addWord(duration); // Duration

  size += addWord(0x00000000); // Language+Quality
addAtomEnd;

addAtom(hdlr);
  size += addWord(0x00000000); // Version + Flags
  size += add4ByteString("mhlr"); // Component type
  size += addWord(fCurrentIOState->fQTcomponentSubtype);
    // Component subtype
  size += add4ByteString("appl"); // Component manufacturer
  size += addWord(0x00000000); // Component flags
  size += addWord(0x00000000); // Component flags mask
  size += addArbitraryString(fCurrentIOState->fQTcomponentName);
    // Component name
addAtomEnd;

addAtom(minf);
  SubsessionIOState::atomCreationFunc mediaInformationAtomCreator
    = fCurrentIOState->fQTMediaInformationAtomCreator;
  size += (this->*mediaInformationAtomCreator)();
  size += addAtom_hdlr2();
  size += addAtom_dinf();
  size += addAtom_stbl();
addAtomEnd;

addAtom(smhd);
  size += addZeroWords(2); // Version+Flags+Balance+Reserved
addAtomEnd;

addAtom(vmhd);
  size += addWord(0x00000001); // Version + Flags
  size += addWord(0x00408000); // Graphics mode + Opcolor[red]
  size += addWord(0x80008000); // Opcolor[green} + Opcolor[blue]
addAtomEnd;

addAtom(gmhd);
  size += addAtom_gmin();
addAtomEnd;

addAtom(gmin);
  size += addWord(0x00000000); // Version + Flags
  // The following fields probably aren't used for hint tracks, so just
  // use values that I've seen in other files:
  size += addWord(0x00408000); // Graphics mode + Opcolor (1st 2 bytes)
  size += addWord(0x80008000); // Opcolor (last 4 bytes)
  size += addWord(0x00000000); // Balance + Reserved
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_hdlr2() {
  int64_t initFilePosn = TellFile64(fOutFid);
  unsigned size = addAtomHeader("hdlr");
  size += addWord(0x00000000); // Version + Flags
  size += add4ByteString("dhlr"); // Component type
  size += add4ByteString("alis"); // Component subtype
  size += add4ByteString("appl"); // Component manufacturer
  size += addZeroWords(2); // Component flags+Component flags mask
  size += addArbitraryString("Apple Alias Data Handler"); // Component name
addAtomEnd;

addAtom(dinf);
  size += addAtom_dref();
addAtomEnd;

addAtom(dref);
  size += addWord(0x00000000); // Version + Flags
  size += addWord(0x00000001); // Number of entries
  size += addAtom_alis();
addAtomEnd;

addAtom(alis);
  size += addWord(0x00000001); // Version + Flags
addAtomEnd;

addAtom(stbl);
  size += addAtom_stsd();
  size += addAtom_stts();
  if (fCurrentIOState->fQTcomponentSubtype == fourChar('v','i','d','e')) {
    size += addAtom_stss(); // only for video streams
  }
  size += addAtom_stsc();
  size += addAtom_stsz();
  size += addAtom_co64();
addAtomEnd;

addAtom(stsd);
  size += addWord(0x00000000); // Version+Flags
  size += addWord(0x00000001); // Number of entries
  SubsessionIOState::atomCreationFunc mediaDataAtomCreator
    = fCurrentIOState->fQTMediaDataAtomCreator;
  size += (this->*mediaDataAtomCreator)();
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_genericMedia() {
  int64_t initFilePosn = TellFile64(fOutFid);

  // Our source is assumed to be a "QuickTimeGenericRTPSource"
  // Use its "sdAtom" state for our contents:
  QuickTimeGenericRTPSource* rtpSource = (QuickTimeGenericRTPSource*)
    fCurrentIOState->fOurSubsession.rtpSource();
  unsigned size = 0;
  if (rtpSource != NULL) {
    QuickTimeGenericRTPSource::QTState& qtState = rtpSource->qtState;
    char const* from = qtState.sdAtom;
    size = qtState.sdAtomSize;
    for (unsigned i = 0; i < size; ++i) addByte(from[i]);
  }
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_soundMediaGeneral() {
  int64_t initFilePosn = TellFile64(fOutFid);
  unsigned size = addAtomHeader(fCurrentIOState->fQTAudioDataType);

// General sample description fields:
  size += addWord(0x00000000); // Reserved
  size += addWord(0x00000001); // Reserved+Data reference index
// Sound sample description fields:
  unsigned short const version = fCurrentIOState->fQTSoundSampleVersion;
  size += addWord(version<<16); // Version+Revision level
  size += addWord(0x00000000); // Vendor
  unsigned short numChannels
    = (unsigned short)(fCurrentIOState->fOurSubsession.numChannels());
  size += addHalfWord(numChannels); // Number of channels
  size += addHalfWord(0x0010); // Sample size
  //  size += addWord(0x00000000); // Compression ID+Packet size
  size += addWord(0xfffe0000); // Compression ID+Packet size #####

  unsigned const sampleRateFixedPoint = fCurrentIOState->fQTTimeScale << 16;
  size += addWord(sampleRateFixedPoint); // Sample rate
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_Qclp() {
  // The beginning of this atom looks just like a general Sound Media atom,
  // except with a version field of 1:
  int64_t initFilePosn = TellFile64(fOutFid);
  fCurrentIOState->fQTAudioDataType = "Qclp";
  fCurrentIOState->fQTSoundSampleVersion = 1;
  unsigned size = addAtom_soundMediaGeneral();

  // Next, add the four fields that are particular to version 1:
  // (Later, parameterize these #####)
  size += addWord(0x000000a0); // samples per packet
  size += addWord(0x00000000); // ???
  size += addWord(0x00000000); // ???
  size += addWord(0x00000002); // bytes per sample (uncompressed)

  // Other special fields are in a 'wave' atom that follows:
  size += addAtom_wave();
addAtomEnd;

addAtom(wave);
  size += addAtom_frma();
  if (strcmp(fCurrentIOState->fQTAudioDataType, "Qclp") == 0) {
    size += addWord(0x00000014); // ???
    size += add4ByteString("Qclp"); // ???
    if (fCurrentIOState->fQTBytesPerFrame == 35) {
      size += addAtom_Fclp(); // full-rate QCELP
    } else {
      size += addAtom_Hclp(); // half-rate QCELP
    } // what about other QCELP 'rates'??? #####
    size += addWord(0x00000008); // ???
    size += addWord(0x00000000); // ???
    size += addWord(0x00000000); // ???
    size += addWord(0x00000008); // ???
  } else if (strcmp(fCurrentIOState->fQTAudioDataType, "mp4a") == 0) {
    size += addWord(0x0000000c); // ???
    size += add4ByteString("mp4a"); // ???
    size += addWord(0x00000000); // ???
    size += addAtom_esds(); // ESDescriptor
    size += addWord(0x00000008); // ???
    size += addWord(0x00000000); // ???
  }
addAtomEnd;

addAtom(frma);
  size += add4ByteString(fCurrentIOState->fQTAudioDataType); // ???
addAtomEnd;

addAtom(Fclp);
 size += addWord(0x00000000); // ???
addAtomEnd;

addAtom(Hclp);
 size += addWord(0x00000000); // ???
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_mp4a() {
  unsigned size = 0;
  // The beginning of this atom looks just like a general Sound Media atom,
  // except with a version field of 1:
  int64_t initFilePosn = TellFile64(fOutFid);
  fCurrentIOState->fQTAudioDataType = "mp4a";

  if (fGenerateMP4Format) {
    fCurrentIOState->fQTSoundSampleVersion = 0;
    size = addAtom_soundMediaGeneral();
    size += addAtom_esds();
  } else {
    fCurrentIOState->fQTSoundSampleVersion = 1;
    size = addAtom_soundMediaGeneral();

    // Next, add the four fields that are particular to version 1:
    // (Later, parameterize these #####)
    size += addWord(fCurrentIOState->fQTTimeUnitsPerSample);
    size += addWord(0x00000001); // ???
    size += addWord(0x00000001); // ???
    size += addWord(0x00000002); // bytes per sample (uncompressed)

    // Other special fields are in a 'wave' atom that follows:
    size += addAtom_wave();
  }
addAtomEnd;

addAtom(esds);
  //#####
  MediaSubsession& subsession = fCurrentIOState->fOurSubsession;
  if (strcmp(subsession.mediumName(), "audio") == 0) {
    // MPEG-4 audio
    size += addWord(0x00000000); // ???
    size += addWord(0x03808080); // ???
    size += addWord(0x2a000000); // ???
    size += addWord(0x04808080); // ???
    size += addWord(0x1c401500); // ???
    size += addWord(0x18000000); // ???
    size += addWord(0x6d600000); // ???
    size += addWord(0x6d600580); // ???
    size += addByte(0x80); size += addByte(0x80); // ???
  } else if (strcmp(subsession.mediumName(), "video") == 0) {
    // MPEG-4 video
    size += addWord(0x00000000); // ???
    size += addWord(0x03330000); // ???
    size += addWord(0x1f042b20); // ???
    size += addWord(0x1104fd46); // ???
    size += addWord(0x000d4e10); // ???
    size += addWord(0x000d4e10); // ???
    size += addByte(0x05); // ???
  }

  // Add the source's 'config' information:
  unsigned configSize;
  unsigned char* config
    = parseGeneralConfigStr(subsession.fmtp_config(), configSize);
  size += addByte(configSize);
  for (unsigned i = 0; i < configSize; ++i) {
    size += addByte(config[i]);
  }
  delete[] config;

  if (strcmp(subsession.mediumName(), "audio") == 0) {
    // MPEG-4 audio
    size += addWord(0x06808080); // ???
    size += addHalfWord(0x0102); // ???
  } else {
    // MPEG-4 video
    size += addHalfWord(0x0601); // ???
    size += addByte(0x02); // ???
  }
  //#####
addAtomEnd;

addAtom(srcq);
  //#####
  size += addWord(0x00000040); // ???
  //#####
addAtomEnd;

addAtom(h263);
// General sample description fields:
  size += addWord(0x00000000); // Reserved
  size += addWord(0x00000001); // Reserved+Data reference index
// Video sample description fields:
  size += addWord(0x00020001); // Version+Revision level
  size += add4ByteString("appl"); // Vendor
  size += addWord(0x00000000); // Temporal quality
  size += addWord(0x000002fc); // Spatial quality
  unsigned const widthAndHeight = (fMovieWidth<<16)|fMovieHeight;
  size += addWord(widthAndHeight); // Width+height
  size += addWord(0x00480000); // Horizontal resolution
  size += addWord(0x00480000); // Vertical resolution
  size += addWord(0x00000000); // Data size
  size += addWord(0x00010548); // Frame count+Compressor name (start)
    // "H.263"
  size += addWord(0x2e323633); // Compressor name (continued)
  size += addZeroWords(6); // Compressor name (continued - zero)
  size += addWord(0x00000018); // Compressor name (final)+Depth
  size += addHalfWord(0xffff); // Color table id
addAtomEnd;

addAtom(avc1);
// General sample description fields:
  size += addWord(0x00000000); // Reserved
  size += addWord(0x00000001); // Reserved+Data       reference index
// Video sample       description     fields:
  size += addWord(0x00000000); // Version+Revision level
  size += add4ByteString("appl"); // Vendor
  size += addWord(0x00000000); // Temporal quality
  size += addWord(0x00000000); // Spatial quality
  unsigned const widthAndHeight       = (fMovieWidth<<16)|fMovieHeight;
  size += addWord(widthAndHeight); // Width+height
  size += addWord(0x00480000); // Horizontal resolution
  size += addWord(0x00480000); // Vertical resolution
  size += addWord(0x00000000); // Data size
  size += addWord(0x00010548); // Frame       count+Compressor name (start)
    // "H.264"
  size += addWord(0x2e323634); // Compressor name (continued)
  size += addZeroWords(6); // Compressor name (continued - zero)
  size += addWord(0x00000018); // Compressor name (final)+Depth
  size += addHalfWord(0xffff); // Color       table id
  size += addAtom_avcC();
addAtomEnd;

addAtom(avcC);
// Begin by Base-64 decoding the "sprop" parameter sets strings:
  char* psets = strDup(fCurrentIOState->fOurSubsession.fmtp_spropparametersets());
  if (psets == NULL) return 0;

  size_t comma_pos = strcspn(psets, ",");
  psets[comma_pos] = '\0';
  char const* sps_b64 = psets;
  char const* pps_b64 = &psets[comma_pos+1];
  unsigned sps_count;
  unsigned char* sps_data = base64Decode(sps_b64, sps_count, false);
  unsigned pps_count;
  unsigned char* pps_data = base64Decode(pps_b64, pps_count, false);

// Then add the decoded data:
  size += addByte(0x01); // configuration version
  size += addByte(sps_data[1]); // profile
  size += addByte(sps_data[2]); // profile compat
  size += addByte(sps_data[3]); // level
  size += addByte(0xff); /* 0b11111100 | lengthsize = 0x11 */
  size += addByte(0xe0 | (sps_count > 0 ? 1 : 0) );
  if (sps_count > 0) {
    size += addHalfWord(sps_count);
    for (unsigned i = 0; i < sps_count; i++) {
      size += addByte(sps_data[i]);
    }
  }
  size += addByte(pps_count > 0 ? 1 : 0);
  if (pps_count > 0) {
    size += addHalfWord(pps_count);
    for (unsigned i = 0; i < pps_count; i++) {
      size += addByte(pps_data[i]);
    }
  }

// Finally, delete the data that we allocated:
  delete[] pps_data; delete[] sps_data;
  delete[] psets;
addAtomEnd;

addAtom(mp4v);
// General sample description fields:
  size += addWord(0x00000000); // Reserved
  size += addWord(0x00000001); // Reserved+Data reference index
// Video sample description fields:
  size += addWord(0x00020001); // Version+Revision level
  size += add4ByteString("appl"); // Vendor
  size += addWord(0x00000200); // Temporal quality
  size += addWord(0x00000400); // Spatial quality
  unsigned const widthAndHeight = (fMovieWidth<<16)|fMovieHeight;
  size += addWord(widthAndHeight); // Width+height
  size += addWord(0x00480000); // Horizontal resolution
  size += addWord(0x00480000); // Vertical resolution
  size += addWord(0x00000000); // Data size
  size += addWord(0x00010c4d); // Frame count+Compressor name (start)
    // "MPEG-4 Video"
  size += addWord(0x5045472d); // Compressor name (continued)
  size += addWord(0x34205669); // Compressor name (continued)
  size += addWord(0x64656f00); // Compressor name (continued)
  size += addZeroWords(4); // Compressor name (continued - zero)
  size += addWord(0x00000018); // Compressor name (final)+Depth
  size += addHalfWord(0xffff); // Color table id
  size += addAtom_esds(); // ESDescriptor
  size += addWord(0x00000000); // ???
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_rtp() {
  int64_t initFilePosn = TellFile64(fOutFid);
  unsigned size = addAtomHeader("rtp ");

  size += addWord(0x00000000); // Reserved (1st 4 bytes)
  size += addWord(0x00000001); // Reserved (last 2 bytes) + Data ref index
  size += addWord(0x00010001); // Hint track version + Last compat htv
  size += addWord(1450); // Max packet size

  size += addAtom_tims();
addAtomEnd;

addAtom(tims);
  size += addWord(fCurrentIOState->fOurSubsession.rtpTimestampFrequency());
addAtomEnd;

addAtom(stts); // Time-to-Sample
  size += addWord(0x00000000); // Version+flags

  // First, add a dummy "Number of entries" field
  // (and remember its position).  We'll fill this field in later:
  int64_t numEntriesPosition = TellFile64(fOutFid);
  size += addWord(0); // dummy for "Number of entries"

  // Then, run through the chunk descriptors, and enter the entries
  // in this (compressed) Time-to-Sample table:
  unsigned numEntries = 0, numSamplesSoFar = 0;
  unsigned prevSampleDuration = 0;
  unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame;
  ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
  while (chunk != NULL) {
    unsigned const sampleDuration = chunk->fFrameDuration/samplesPerFrame;
    if (sampleDuration != prevSampleDuration) {
      // This chunk will start a new table entry,
      // so write out the old one (if any):
      if (chunk != fCurrentIOState->fHeadChunk) {
	++numEntries;
	size += addWord(numSamplesSoFar); // Sample count
	size += addWord(prevSampleDuration); // Sample duration
	numSamplesSoFar = 0;
      }
    }

    unsigned const numSamples = chunk->fNumFrames*samplesPerFrame;
    numSamplesSoFar += numSamples;
    prevSampleDuration = sampleDuration;
    chunk = chunk->fNextChunk;
  }

  // Then, write out the last entry:
  ++numEntries;
  size += addWord(numSamplesSoFar); // Sample count
  size += addWord(prevSampleDuration); // Sample duration

  // Now go back and fill in the "Number of entries" field:
  setWord(numEntriesPosition, numEntries);
addAtomEnd;

addAtom(stss); // Sync-Sample
  size += addWord(0x00000000); // Version+flags

  // First, add a dummy "Number of entries" field
  // (and remember its position).  We'll fill this field in later:
  int64_t numEntriesPosition = TellFile64(fOutFid);
  size += addWord(0); // dummy for "Number of entries"

  unsigned numEntries = 0, numSamplesSoFar = 0;
  if (fCurrentIOState->fHeadSyncFrame != NULL) {
    SyncFrame* currentSyncFrame = fCurrentIOState->fHeadSyncFrame;

    // First, count the number of frames (to use as a sanity check; see below):
    unsigned totNumFrames = 0;
    for (ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; chunk != NULL; chunk = chunk->fNextChunk) totNumFrames += chunk->fNumFrames;

    while (currentSyncFrame != NULL) {
      if (currentSyncFrame->sfFrameNum >= totNumFrames) break; // sanity check
      
      ++numEntries;
      size += addWord(currentSyncFrame->sfFrameNum);
      currentSyncFrame = currentSyncFrame->nextSyncFrame;
    }
  } else {
    // First, run through the chunk descriptors, counting up the total number of samples:
    unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame;
    ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
    while (chunk != NULL) {
      unsigned const numSamples = chunk->fNumFrames*samplesPerFrame;
      numSamplesSoFar += numSamples;
      chunk = chunk->fNextChunk;
    }
  
    // Then, write out the sample numbers that we deem correspond to 'sync samples':
    unsigned i;
    for (i = 0; i < numSamplesSoFar; i += 12) {
      // For an explanation of the constant "12", see http://lists.live555.com/pipermail/live-devel/2009-July/010969.html
      // (Perhaps we should really try to keep track of which 'samples' ('frames' for video) really are 'key frames'?)
      size += addWord(i+1);
      ++numEntries;
    }
  
    // Then, write out the last entry (if we haven't already done so):
    if (i != (numSamplesSoFar - 1)) {
      size += addWord(numSamplesSoFar);
      ++numEntries;
    }
  }

  // Now go back and fill in the "Number of entries" field:
  setWord(numEntriesPosition, numEntries);
addAtomEnd;

addAtom(stsc); // Sample-to-Chunk
  size += addWord(0x00000000); // Version+flags

  // First, add a dummy "Number of entries" field
  // (and remember its position).  We'll fill this field in later:
  int64_t numEntriesPosition = TellFile64(fOutFid);
  size += addWord(0); // dummy for "Number of entries"

  // Then, run through the chunk descriptors, and enter the entries
  // in this (compressed) Sample-to-Chunk table:
  unsigned numEntries = 0, chunkNumber = 0;
  unsigned prevSamplesPerChunk = ~0;
  unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame;
  ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
  while (chunk != NULL) {
    ++chunkNumber;
    unsigned const samplesPerChunk = chunk->fNumFrames*samplesPerFrame;
    if (samplesPerChunk != prevSamplesPerChunk) {
      // This chunk will be a new table entry:
      ++numEntries;
      size += addWord(chunkNumber); // Chunk number
      size += addWord(samplesPerChunk); // Samples per chunk
      size += addWord(0x00000001); // Sample description ID

      prevSamplesPerChunk = samplesPerChunk;
    }
    chunk = chunk->fNextChunk;
  }

  // Now go back and fill in the "Number of entries" field:
  setWord(numEntriesPosition, numEntries);
addAtomEnd;

addAtom(stsz); // Sample Size
  size += addWord(0x00000000); // Version+flags

  // Begin by checking whether our chunks all have the same
  // 'bytes-per-sample'.  This determines whether this atom's table
  // has just a single entry, or multiple entries.
  Boolean haveSingleEntryTable = True;
  double firstBPS = 0.0;
  ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
  while (chunk != NULL) {
    double bps
      = (double)(chunk->fFrameSize)/(fCurrentIOState->fQTSamplesPerFrame);
    if (bps < 1.0) {
      // I don't think a multiple-entry table would make sense in
      // this case, so assume a single entry table ??? #####
      break;
    }

    if (firstBPS == 0.0) {
      firstBPS = bps;
    } else if (bps != firstBPS) {
      haveSingleEntryTable = False;
      break;
    }

    chunk = chunk->fNextChunk;
  }

  unsigned sampleSize;
  if (haveSingleEntryTable) {
    if (fCurrentIOState->isHintTrack()
	&& fCurrentIOState->fHeadChunk != NULL) {
      sampleSize = fCurrentIOState->fHeadChunk->fFrameSize
	              / fCurrentIOState->fQTSamplesPerFrame;
    } else {
      // The following doesn't seem right, but seems to do the right thing:
      sampleSize = fCurrentIOState->fQTTimeUnitsPerSample; //???
    }
  } else {
    sampleSize = 0; // indicates a multiple-entry table
  }
  size += addWord(sampleSize); // Sample size
  unsigned const totNumSamples = fCurrentIOState->fQTTotNumSamples;
  size += addWord(totNumSamples); // Number of entries

  if (!haveSingleEntryTable) {
    // Multiple-entry table:
    // Run through the chunk descriptors, entering the sample sizes:
    ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
    while (chunk != NULL) {
      unsigned numSamples
	= chunk->fNumFrames*(fCurrentIOState->fQTSamplesPerFrame);
      unsigned sampleSize
	= chunk->fFrameSize/(fCurrentIOState->fQTSamplesPerFrame);
      for (unsigned i = 0; i < numSamples; ++i) {
	size += addWord(sampleSize);
      }

      chunk = chunk->fNextChunk;
    }
  }
addAtomEnd;

addAtom(co64); // Chunk Offset
  size += addWord(0x00000000); // Version+flags
  size += addWord(fCurrentIOState->fNumChunks); // Number of entries

  // Run through the chunk descriptors, entering the file offsets:
  ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk;
  while (chunk != NULL) {
    size += addWord64(chunk->fOffsetInFile);

    chunk = chunk->fNextChunk;
  }
addAtomEnd;

addAtom(udta);
  size += addAtom_name();
  size += addAtom_hnti();
  size += addAtom_hinf();
addAtomEnd;

addAtom(name);
  char description[100];
  sprintf(description, "Hinted %s track",
	  fCurrentIOState->fOurSubsession.mediumName());
  size += addArbitraryString(description, False); // name of object
addAtomEnd;

addAtom(hnti);
  size += addAtom_sdp();
addAtomEnd;

unsigned QuickTimeFileSink::addAtom_sdp() {
  int64_t initFilePosn = TellFile64(fOutFid);
  unsigned size = addAtomHeader("sdp ");

  // Add this subsession's SDP lines:
  char const* sdpLines = fCurrentIOState->fOurSubsession.savedSDPLines();
  // We need to change any "a=control:trackID=" values to be this
  // track's actual track id:
  char* newSDPLines = new char[strlen(sdpLines)+100/*overkill*/];
  char const* searchStr = "a=control:trackid=";
  Boolean foundSearchString = False;
  char const *p1, *p2, *p3;
  for (p1 = sdpLines; *p1 != '\0'; ++p1) {
    for (p2 = p1,p3 = searchStr; tolower(*p2) == *p3; ++p2,++p3) {}
    if (*p3 == '\0') {
      // We found the end of the search string, at p2.
      int beforeTrackNumPosn = p2-sdpLines;
      // Look for the subsequent track number, and skip over it:
      int trackNumLength;
      if (sscanf(p2, " %*d%n", &trackNumLength) < 0) break;
      int afterTrackNumPosn = beforeTrackNumPosn + trackNumLength;

      // Replace the old track number with the correct one:
      int i;
      for (i = 0; i < beforeTrackNumPosn; ++i) newSDPLines[i] = sdpLines[i];
      sprintf(&newSDPLines[i], "%d", fCurrentIOState->fTrackID);
      i = afterTrackNumPosn;
      int j = i + strlen(&newSDPLines[i]);
      while (1) {
	if ((newSDPLines[j] = sdpLines[i]) == '\0') break;
	++i; ++j;
      }

      foundSearchString = True;
      break;
    }
  }

  if (!foundSearchString) {
    // Because we didn't find a "a=control:trackID=<trackId>" line,
    // add one of our own:
    sprintf(newSDPLines, "%s%s%d\r\n",
	    sdpLines, searchStr, fCurrentIOState->fTrackID);
  }

  size += addArbitraryString(newSDPLines, False);
  delete[] newSDPLines;
addAtomEnd;

addAtom(hinf);
  size += addAtom_totl();
  size += addAtom_npck();
  size += addAtom_tpay();
  size += addAtom_trpy();
  size += addAtom_nump();
  size += addAtom_tpyl();
  // Is 'maxr' required? #####
  size += addAtom_dmed();
  size += addAtom_dimm();
  size += addAtom_drep();
  size += addAtom_tmin();
  size += addAtom_tmax();
  size += addAtom_pmax();
  size += addAtom_dmax();
  size += addAtom_payt();
addAtomEnd;

addAtom(totl);
 size += addWord(fCurrentIOState->fHINF.trpy.lo);
addAtomEnd;

addAtom(npck);
 size += addWord(fCurrentIOState->fHINF.nump.lo);
addAtomEnd;

addAtom(tpay);
 size += addWord(fCurrentIOState->fHINF.tpyl.lo);
addAtomEnd;

addAtom(trpy);
 size += addWord(fCurrentIOState->fHINF.trpy.hi);
 size += addWord(fCurrentIOState->fHINF.trpy.lo);
addAtomEnd;

addAtom(nump);
 size += addWord(fCurrentIOState->fHINF.nump.hi);
 size += addWord(fCurrentIOState->fHINF.nump.lo);
addAtomEnd;

addAtom(tpyl);
 size += addWord(fCurrentIOState->fHINF.tpyl.hi);
 size += addWord(fCurrentIOState->fHINF.tpyl.lo);
addAtomEnd;

addAtom(dmed);
 size += addWord(fCurrentIOState->fHINF.dmed.hi);
 size += addWord(fCurrentIOState->fHINF.dmed.lo);
addAtomEnd;

addAtom(dimm);
 size += addWord(fCurrentIOState->fHINF.dimm.hi);
 size += addWord(fCurrentIOState->fHINF.dimm.lo);
addAtomEnd;

addAtom(drep);
 size += addWord(0);
 size += addWord(0);
addAtomEnd;

addAtom(tmin);
 size += addWord(0);
addAtomEnd;

addAtom(tmax);
 size += addWord(0);
addAtomEnd;

addAtom(pmax);
 size += addWord(fCurrentIOState->fHINF.pmax);
addAtomEnd;

addAtom(dmax);
 size += addWord(fCurrentIOState->fHINF.dmax);
addAtomEnd;

addAtom(payt);
  MediaSubsession& ourSubsession = fCurrentIOState->fOurSubsession;
  RTPSource* rtpSource = ourSubsession.rtpSource();
  if (rtpSource != NULL) {
    size += addWord(rtpSource->rtpPayloadFormat());

    // Also, add a 'rtpmap' string: <mime-subtype>/<rtp-frequency>
    unsigned rtpmapStringLength = strlen(ourSubsession.codecName()) + 20;
    char* rtpmapString = new char[rtpmapStringLength];
    sprintf(rtpmapString, "%s/%d",
	    ourSubsession.codecName(), rtpSource->timestampFrequency());
    size += addArbitraryString(rtpmapString);
    delete[] rtpmapString;
  }
addAtomEnd;

// A dummy atom (with name "????"):
unsigned QuickTimeFileSink::addAtom_dummy() {
    int64_t initFilePosn = TellFile64(fOutFid);
    unsigned size = addAtomHeader("????");
addAtomEnd;
