/**********
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 3 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-2026 Live Networks, Inc.  All rights reserved.
// A 'ServerMediaSubsession' object that creates new, unicast, "RTPSink"s
// on demand, from a MPEG-2 Transport Stream file.
// Implementation

#include "MPEG2TransportFileServerMediaSubsession.hh"
#include "SimpleRTPSink.hh"

MPEG2TransportFileServerMediaSubsession*
MPEG2TransportFileServerMediaSubsession::createNew(UsageEnvironment& env,
						   char const* fileName,
						   char const* indexFileName,
						   Boolean reuseFirstSource) {
  MPEG2TransportStreamIndexFile* indexFile;
  if (indexFileName != NULL && reuseFirstSource) {
    // It makes no sense to support trick play if all clients use the same source.  Fix this:
    env << "MPEG2TransportFileServerMediaSubsession::createNew(): ignoring the index file name, because \"reuseFirstSource\" is set\n";
    indexFile = NULL;
  } else {
    indexFile = MPEG2TransportStreamIndexFile::createNew(env, indexFileName);
  }
  return new MPEG2TransportFileServerMediaSubsession(env, fileName, indexFile,
						     reuseFirstSource);
}

MPEG2TransportFileServerMediaSubsession
::MPEG2TransportFileServerMediaSubsession(UsageEnvironment& env,
					  char const* fileName,
					  MPEG2TransportStreamIndexFile* indexFile,
					  Boolean reuseFirstSource)
  : FileServerMediaSubsession(env, fileName, reuseFirstSource),
    fIndexFile(indexFile), fDuration(0.0), fClientSessionHashTable(NULL) {
  if (fIndexFile != NULL) { // we support 'trick play'
    fDuration = fIndexFile->getPlayingDuration();
    fClientSessionHashTable = HashTable::create(ONE_WORD_HASH_KEYS);
  }
}

MPEG2TransportFileServerMediaSubsession
::~MPEG2TransportFileServerMediaSubsession() {
  if (fIndexFile != NULL) { // we support 'trick play'
    Medium::close(fIndexFile);

    // Clean out the client session hash table:
    while (1) {
      ClientTrickPlayState* client
	= (ClientTrickPlayState*)(fClientSessionHashTable->RemoveNext());
      if (client == NULL) break;
      delete client;
    }
    delete fClientSessionHashTable;
  }
}

#define TRANSPORT_PACKET_SIZE 188
#define TRANSPORT_PACKETS_PER_NETWORK_PACKET 7
// The product of these two numbers must be enough to fit within a network packet

void MPEG2TransportFileServerMediaSubsession
::startStream(unsigned clientSessionId, void* streamToken, TaskFunc* rtcpRRHandler,
	      void* rtcpRRHandlerClientData, unsigned short& rtpSeqNum,
	      unsigned& rtpTimestamp,
	      ServerRequestAlternativeByteHandler* serverRequestAlternativeByteHandler,
	      void* serverRequestAlternativeByteHandlerClientData) {
  if (fIndexFile != NULL) { // we support 'trick play'
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client != NULL && client->areChangingScale()) {
      // First, handle this like a "PAUSE", except that we back up to the previous VSH
      client->updateStateOnPlayChange(True);
      OnDemandServerMediaSubsession::pauseStream(clientSessionId, streamToken);

      // Then, adjust for the change of scale:
      client->updateStateOnScaleChange();
    }
  }

  // Call the original, default version of this routine:
  OnDemandServerMediaSubsession::startStream(clientSessionId, streamToken,
					     rtcpRRHandler, rtcpRRHandlerClientData,
					     rtpSeqNum, rtpTimestamp,
					     serverRequestAlternativeByteHandler, serverRequestAlternativeByteHandlerClientData);
}

void MPEG2TransportFileServerMediaSubsession
::pauseStream(unsigned clientSessionId, void* streamToken) {
  if (fIndexFile != NULL) { // we support 'trick play'
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client != NULL) {
      client->updateStateOnPlayChange(False);
    }
  }

  // Call the original, default version of this routine:
  OnDemandServerMediaSubsession::pauseStream(clientSessionId, streamToken);
}

void MPEG2TransportFileServerMediaSubsession
::seekStream(unsigned clientSessionId, void* streamToken, double& seekNPT, double streamDuration, u_int64_t& numBytes) {
  // Begin by calling the original, default version of this routine:
  OnDemandServerMediaSubsession::seekStream(clientSessionId, streamToken, seekNPT, streamDuration, numBytes);

  // Then, special handling specific to indexed Transport Stream files:
  if (fIndexFile != NULL) { // we support 'trick play'
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client != NULL) {
      unsigned long numTSPacketsToStream = client->updateStateFromNPT(seekNPT, streamDuration);
      numBytes = numTSPacketsToStream*TRANSPORT_PACKET_SIZE;
    }
  }
}

void MPEG2TransportFileServerMediaSubsession
::setStreamScale(unsigned clientSessionId, void* streamToken, float scale) {
  if (fIndexFile != NULL) { // we support 'trick play'
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client != NULL) {
      client->setNextScale(scale); // scale won't take effect until the next "PLAY"
    }
  }

  // Call the original, default version of this routine:
  OnDemandServerMediaSubsession::setStreamScale(clientSessionId, streamToken, scale);
}

void MPEG2TransportFileServerMediaSubsession
::deleteStream(unsigned clientSessionId, void*& streamToken) {
  if (fIndexFile != NULL) { // we support 'trick play'
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client != NULL) {
      client->updateStateOnPlayChange(False);
    }
  }

  // Call the original, default version of this routine:
  OnDemandServerMediaSubsession::deleteStream(clientSessionId, streamToken);
}

ClientTrickPlayState* MPEG2TransportFileServerMediaSubsession::newClientTrickPlayState() {
  return new ClientTrickPlayState(fIndexFile);
}

FramedSource* MPEG2TransportFileServerMediaSubsession
::createNewStreamSource(unsigned clientSessionId, unsigned& estBitrate) {
  // Create the video source:
  unsigned const inputDataChunkSize
    = TRANSPORT_PACKETS_PER_NETWORK_PACKET*TRANSPORT_PACKET_SIZE;
  ByteStreamFileSource* fileSource
    = ByteStreamFileSource::createNew(envir(), fFileName, inputDataChunkSize);
  if (fileSource == NULL) return NULL;
  fFileSize = fileSource->fileSize();

  // Use the file size and the duration to estimate the stream's bitrate:
  if (fFileSize > 0 && fDuration > 0.0) {
    estBitrate = (unsigned)((int64_t)fFileSize/(125*fDuration) + 0.5); // kbps, rounded
  } else {
    estBitrate = 5000; // kbps, estimate
  }


  // Create a framer for the Transport Stream:
  MPEG2TransportStreamFramer* framer
    = MPEG2TransportStreamFramer::createNew(envir(), fileSource);

  if (fIndexFile != NULL) { // we support 'trick play'
    // Keep state for this client (if we don't already have it):
    ClientTrickPlayState* client = lookupClient(clientSessionId);
    if (client == NULL) {
      client = newClientTrickPlayState();
      fClientSessionHashTable->Add((char const*)clientSessionId, client);
    }
    client->setSource(framer);
  }

  return framer;
}

RTPSink* MPEG2TransportFileServerMediaSubsession
::createNewRTPSink(Groupsock* rtpGroupsock,
		   unsigned char /*rtpPayloadTypeIfDynamic*/,
		   FramedSource* /*inputSource*/) {
  return SimpleRTPSink::createNew(envir(), rtpGroupsock,
				  33, 90000, "video", "MP2T",
				  1, True, False /*no 'M' bit*/);
}

void MPEG2TransportFileServerMediaSubsession::testScaleFactor(float& scale) {
  if (fIndexFile != NULL && fDuration > 0.0) {
    // We support any integral scale, other than 0
    int iScale = scale < 0.0 ? (int)(scale - 0.5f) : (int)(scale + 0.5f); // round
    if (iScale == 0) iScale = 1;
    scale = (float)iScale;
  } else {
    scale = 1.0f;
  }
}

float MPEG2TransportFileServerMediaSubsession::duration() const {
  return fDuration;
}

ClientTrickPlayState* MPEG2TransportFileServerMediaSubsession
::lookupClient(unsigned clientSessionId) {
  return (ClientTrickPlayState*)(fClientSessionHashTable->Lookup((char const*)clientSessionId));
}


////////// ClientTrickPlayState implementation //////////

ClientTrickPlayState::ClientTrickPlayState(MPEG2TransportStreamIndexFile* indexFile)
  : fIndexFile(indexFile),
    fOriginalTransportStreamSource(NULL),
    fTrickModeFilter(NULL), fTrickPlaySource(NULL),
    fFramer(NULL),
    fScale(1.0f), fNextScale(1.0f), fNPT(0.0f),
    fTSRecordNum(0), fIxRecordNum(0) {
}

unsigned long ClientTrickPlayState::updateStateFromNPT(double npt, double streamDuration) {
  fNPT = (float)npt;
  // Map "fNPT" to the corresponding Transport Stream and Index record numbers:
  unsigned long tsRecordNum, ixRecordNum;
  fIndexFile->lookupTSPacketNumFromNPT(fNPT, tsRecordNum, ixRecordNum);

  updateTSRecordNum();
  if (tsRecordNum != fTSRecordNum) {
    fTSRecordNum = tsRecordNum;
    fIxRecordNum = ixRecordNum;

    // Seek the source to the new record number:
    reseekOriginalTransportStreamSource();
    // Note: We assume that we're asked to seek only in normal
    // (i.e., non trick play) mode, so we don't seek within the trick
    // play source (if any).

    fFramer->clearPIDStatusTable();
  }

  unsigned long numTSRecordsToStream = 0;
  float pcrLimit = 0.0;
  if (streamDuration > 0.0) {
    // fNPT might have changed when we looked it up in the index file.  Adjust "streamDuration" accordingly:
    streamDuration += npt - (double)fNPT;

    if (streamDuration > 0.0) {
      // Specify that we want to stream no more data than this.

      if (fNextScale == 1.0f) {
	// We'll be streaming from the original file.
	// Use the index file to figure out how many Transport Packets we get to stream:
	unsigned long toTSRecordNum, toIxRecordNum;    
	float toNPT = (float)(fNPT + streamDuration);
	fIndexFile->lookupTSPacketNumFromNPT(toNPT, toTSRecordNum, toIxRecordNum);
	if (toTSRecordNum > tsRecordNum) { // sanity check
	  numTSRecordsToStream = toTSRecordNum - tsRecordNum;
	}
      } else {
	// We'll be streaming from the trick play stream.  
	// It'd be difficult to figure out how many Transport Packets we need to stream, so instead set a PCR
	// limit in the trick play stream.  (We rely upon the fact that PCRs in the trick play stream start at 0.0)
	int direction = fNextScale < 0.0 ? -1 : 1;
	pcrLimit = (float)(streamDuration/(fNextScale*direction));
      }
    }
  }
  fFramer->setNumTSPacketsToStream(numTSRecordsToStream);
  fFramer->setPCRLimit(pcrLimit);

  return numTSRecordsToStream;
}

void ClientTrickPlayState::updateStateOnScaleChange() {
  fScale = fNextScale;

  // Change our source objects to reflect the change in scale:
  // First, close the existing trick play source (if any):
  if (fTrickPlaySource != NULL) {
    fTrickModeFilter->forgetInputSource();
        // so that the underlying Transport Stream source doesn't get deleted by:
    Medium::close(fTrickPlaySource);
    fTrickPlaySource = NULL;
    fTrickModeFilter = NULL;
  }
  if (fNextScale != 1.0f) {
    // Create a new trick play filter from the original Transport Stream source:
    UsageEnvironment& env = fIndexFile->envir(); // alias
    fTrickModeFilter = MPEG2TransportStreamTrickModeFilter
      ::createNew(env, fOriginalTransportStreamSource, fIndexFile, int(fNextScale));
    fTrickModeFilter->seekTo(fTSRecordNum, fIxRecordNum);

    // And generate a Transport Stream from this:
    fTrickPlaySource = MPEG2TransportStreamFromESSource::createNew(env);
    fTrickPlaySource->addNewVideoSource(fTrickModeFilter, fIndexFile->mpegVersion());

    fFramer->changeInputSource(fTrickPlaySource);
  } else {
    // Switch back to the original Transport Stream source:
    reseekOriginalTransportStreamSource();
    fFramer->changeInputSource(fOriginalTransportStreamSource);
  }
}

void ClientTrickPlayState::updateStateOnPlayChange(Boolean reverseToPreviousVSH) {
  updateTSRecordNum();
  if (fTrickPlaySource == NULL) {
    // We were in regular (1x) play. Use the index file to look up the
    // index record number and npt from the current transport number:
    fIndexFile->lookupPCRFromTSPacketNum(fTSRecordNum, reverseToPreviousVSH, fNPT, fIxRecordNum);
  } else {
    // We were in trick mode, and so already have the index record number.
    // Get the transport record number and npt from this:
    fIxRecordNum = fTrickModeFilter->nextIndexRecordNum();
    if ((long)fIxRecordNum < 0) fIxRecordNum = 0; // we were at the start of the file
    unsigned long transportRecordNum;
    float pcr;
    u_int8_t offset, size, recordType; // all dummy
    if (fIndexFile->readIndexRecordValues(fIxRecordNum, transportRecordNum,
					  offset, size, pcr, recordType)) {
      fTSRecordNum = transportRecordNum;
      fNPT = pcr;
    }
  }
}

void ClientTrickPlayState::setSource(MPEG2TransportStreamFramer* framer) {
  fFramer = framer;
  fOriginalTransportStreamSource = (ByteStreamFileSource*)(framer->inputSource());
}

void ClientTrickPlayState::updateTSRecordNum(){
  if (fFramer != NULL) fTSRecordNum += (unsigned long)(fFramer->tsPacketCount());
}

void ClientTrickPlayState::reseekOriginalTransportStreamSource() {
  u_int64_t tsRecordNum64 = (u_int64_t)fTSRecordNum;
  fOriginalTransportStreamSource->seekToByteAbsolute(tsRecordNum64*TRANSPORT_PACKET_SIZE);
}
