| /********** |
| 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-2020 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); |
| } |