| /********** |
| 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-2015 Live Networks, Inc. All rights reserved. |
| // 'Ogg' File Sink (recording a single media track only) |
| // Implementation |
| |
| #include "OggFileSink.hh" |
| #include "OutputFile.hh" |
| #include "VorbisAudioRTPSource.hh" // for "parseVorbisOrTheoraConfigStr()" |
| #include "MPEG2TransportStreamMultiplexor.hh" // for calculateCRC() |
| #include "FramedSource.hh" |
| |
| OggFileSink* OggFileSink |
| ::createNew(UsageEnvironment& env, char const* fileName, |
| unsigned samplingFrequency, char const* configStr, |
| unsigned bufferSize, Boolean oneFilePerFrame) { |
| do { |
| FILE* fid; |
| char const* perFrameFileNamePrefix; |
| if (oneFilePerFrame) { |
| // Create the fid for each frame |
| fid = NULL; |
| perFrameFileNamePrefix = fileName; |
| } else { |
| // Normal case: create the fid once |
| fid = OpenOutputFile(env, fileName); |
| if (fid == NULL) break; |
| perFrameFileNamePrefix = NULL; |
| } |
| |
| return new OggFileSink(env, fid, samplingFrequency, configStr, bufferSize, perFrameFileNamePrefix); |
| } while (0); |
| |
| return NULL; |
| } |
| |
| OggFileSink::OggFileSink(UsageEnvironment& env, FILE* fid, |
| unsigned samplingFrequency, char const* configStr, |
| unsigned bufferSize, char const* perFrameFileNamePrefix) |
| : FileSink(env, fid, bufferSize, perFrameFileNamePrefix), |
| fSamplingFrequency(samplingFrequency), fConfigStr(configStr), |
| fHaveWrittenFirstFrame(False), fHaveSeenEOF(False), |
| fGranulePosition(0), fGranulePositionAdjustment(0), fPageSequenceNumber(0), |
| fIsTheora(False), fGranuleIncrementPerFrame(1), |
| fAltFrameSize(0), fAltNumTruncatedBytes(0) { |
| fAltBuffer = new unsigned char[bufferSize]; |
| |
| // Initialize our 'Ogg page header' array with constant values: |
| u_int8_t* p = fPageHeaderBytes; |
| *p++=0x4f; *p++=0x67; *p++=0x67; *p++=0x53; // bytes 0..3: 'capture_pattern': "OggS" |
| *p++=0; // byte 4: 'stream_structure_version': 0 |
| *p++=0; // byte 5: 'header_type_flag': set on each write |
| *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; *p++=0; |
| // bytes 6..13: 'granule_position': set on each write |
| *p++=1; *p++=0; *p++=0; *p++=0; // bytes 14..17: 'bitstream_serial_number': 1 |
| *p++=0; *p++=0; *p++=0; *p++=0; // bytes 18..21: 'page_sequence_number': set on each write |
| *p++=0; *p++=0; *p++=0; *p++=0; // bytes 22..25: 'CRC_checksum': set on each write |
| *p=0; // byte 26: 'number_page_segments': set on each write |
| } |
| |
| OggFileSink::~OggFileSink() { |
| // We still have the previously-arrived frame, so write it to the file before we end: |
| fHaveSeenEOF = True; |
| OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); |
| |
| delete[] fAltBuffer; |
| } |
| |
| Boolean OggFileSink::continuePlaying() { |
| // Identical to "FileSink::continuePlaying()", |
| // except that we use our own 'on source closure' function: |
| if (fSource == NULL) return False; |
| |
| fSource->getNextFrame(fBuffer, fBufferSize, |
| FileSink::afterGettingFrame, this, |
| ourOnSourceClosure, this); |
| return True; |
| } |
| |
| #define PAGE_DATA_MAX_SIZE (255*255) |
| |
| void OggFileSink::addData(unsigned char const* data, unsigned dataSize, |
| struct timeval presentationTime) { |
| if (dataSize == 0) return; |
| |
| // Set "fGranulePosition" for this frame: |
| if (fIsTheora) { |
| // Special case for Theora: "fGranulePosition" is supposed to be made up of a pair: |
| // (frame count to last key frame) | (frame count since last key frame) |
| // However, because there appears to be no easy way to figure out which frames are key frames, |
| // we just assume that all frames are key frames. |
| if (!(data[0] >= 0x80 && data[0] <= 0x82)) { // for header pages, "fGranulePosition" remains 0 |
| fGranulePosition += fGranuleIncrementPerFrame; |
| } |
| } else { |
| double ptDiff |
| = (presentationTime.tv_sec - fFirstPresentationTime.tv_sec) |
| + (presentationTime.tv_usec - fFirstPresentationTime.tv_usec)/1000000.0; |
| int64_t newGranulePosition |
| = (int64_t)(fSamplingFrequency*ptDiff) + fGranulePositionAdjustment; |
| if (newGranulePosition < fGranulePosition) { |
| // Update "fGranulePositionAdjustment" so that "fGranulePosition" remains monotonic |
| fGranulePositionAdjustment += fGranulePosition - newGranulePosition; |
| } else { |
| fGranulePosition = newGranulePosition; |
| } |
| } |
| |
| // Write the frame to the file as a single Ogg 'page' (or perhaps as multiple pages |
| // if it's too big for a single page). We don't aggregate more than one frame within |
| // an Ogg page because that's not legal for some headers, and because that would make |
| // it difficult for us to properly set the 'eos' (end of stream) flag on the last page. |
| |
| // First, figure out how many pages to write here |
| // (a page can contain no more than PAGE_DATA_MAX_SIZE bytes) |
| unsigned numPagesToWrite = dataSize/PAGE_DATA_MAX_SIZE + 1; |
| // Note that if "dataSize" is a integral multiple of PAGE_DATA_MAX_SIZE, there will |
| // be an extra 0-size page at the end |
| for (unsigned i = 0; i < numPagesToWrite; ++i) { |
| // First, fill in the changeable parts of our 'page header' array; |
| u_int8_t header_type_flag = 0x0; |
| if (!fHaveWrittenFirstFrame && i == 0) { |
| header_type_flag |= 0x02; // 'bos' |
| fHaveWrittenFirstFrame = True; // for the future |
| } |
| if (i > 0) header_type_flag |= 0x01; // 'continuation' |
| if (fHaveSeenEOF && i == numPagesToWrite-1) header_type_flag |= 0x04; // 'eos' |
| fPageHeaderBytes[5] = header_type_flag; |
| |
| if (i < numPagesToWrite-1) { |
| // For pages where the frame does not end, set 'granule_position' in the header to -1: |
| fPageHeaderBytes[6] = fPageHeaderBytes[7] = fPageHeaderBytes[8] = fPageHeaderBytes[9] = |
| fPageHeaderBytes[10] = fPageHeaderBytes[11] = fPageHeaderBytes[12] = fPageHeaderBytes[13] |
| = 0xFF; |
| } else { |
| fPageHeaderBytes[6] = (u_int8_t)fGranulePosition; |
| fPageHeaderBytes[7] = (u_int8_t)(fGranulePosition>>8); |
| fPageHeaderBytes[8] = (u_int8_t)(fGranulePosition>>16); |
| fPageHeaderBytes[9] = (u_int8_t)(fGranulePosition>>24); |
| fPageHeaderBytes[10] = (u_int8_t)(fGranulePosition>>32); |
| fPageHeaderBytes[11] = (u_int8_t)(fGranulePosition>>40); |
| fPageHeaderBytes[12] = (u_int8_t)(fGranulePosition>>48); |
| fPageHeaderBytes[13] = (u_int8_t)(fGranulePosition>>56); |
| } |
| |
| fPageHeaderBytes[18] = (u_int8_t)fPageSequenceNumber; |
| fPageHeaderBytes[19] = (u_int8_t)(fPageSequenceNumber>>8); |
| fPageHeaderBytes[20] = (u_int8_t)(fPageSequenceNumber>>16); |
| fPageHeaderBytes[21] = (u_int8_t)(fPageSequenceNumber>>24); |
| ++fPageSequenceNumber; |
| |
| unsigned pageDataSize; |
| u_int8_t number_page_segments; |
| if (dataSize >= PAGE_DATA_MAX_SIZE) { |
| pageDataSize = PAGE_DATA_MAX_SIZE; |
| number_page_segments = 255; |
| } else { |
| pageDataSize = dataSize; |
| number_page_segments = (pageDataSize+255)/255; // so that we don't end with a lacing of 255 |
| } |
| fPageHeaderBytes[26] = number_page_segments; |
| |
| u_int8_t segment_table[255]; |
| for (unsigned j = 0; j < (unsigned)(number_page_segments-1); ++j) { |
| segment_table[j] = 255; |
| } |
| segment_table[number_page_segments-1] = pageDataSize%255; |
| |
| // Compute the CRC from the 'page header' array, the 'segment_table', and the frame data: |
| u_int32_t crc = 0; |
| fPageHeaderBytes[22] = fPageHeaderBytes[23] = fPageHeaderBytes[24] = fPageHeaderBytes[25] = 0; |
| crc = calculateCRC(fPageHeaderBytes, 27, 0); |
| crc = calculateCRC(segment_table, number_page_segments, crc); |
| crc = calculateCRC(data, pageDataSize, crc); |
| fPageHeaderBytes[22] = (u_int8_t)crc; |
| fPageHeaderBytes[23] = (u_int8_t)(crc>>8); |
| fPageHeaderBytes[24] = (u_int8_t)(crc>>16); |
| fPageHeaderBytes[25] = (u_int8_t)(crc>>24); |
| |
| // Then write out the 'page header' array: |
| FileSink::addData(fPageHeaderBytes, 27, presentationTime); |
| |
| // Then write out the 'segment_table': |
| FileSink::addData(segment_table, number_page_segments, presentationTime); |
| |
| // Then add frame data, to complete the page: |
| FileSink::addData(data, pageDataSize, presentationTime); |
| data += pageDataSize; |
| dataSize -= pageDataSize; |
| } |
| } |
| |
| void OggFileSink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime) { |
| if (!fHaveWrittenFirstFrame) { |
| fFirstPresentationTime = presentationTime; |
| |
| // If we have a 'config string' representing 'packed configuration headers' |
| // ("identification", "comment", "setup"), unpack them and prepend them to the file: |
| if (fConfigStr != NULL && fConfigStr[0] != '\0') { |
| u_int8_t* identificationHdr; unsigned identificationHdrSize; |
| u_int8_t* commentHdr; unsigned commentHdrSize; |
| u_int8_t* setupHdr; unsigned setupHdrSize; |
| u_int32_t identField; |
| parseVorbisOrTheoraConfigStr(fConfigStr, |
| identificationHdr, identificationHdrSize, |
| commentHdr, commentHdrSize, |
| setupHdr, setupHdrSize, |
| identField); |
| if (identificationHdrSize >= 42 |
| && strncmp((const char*)&identificationHdr[1], "theora", 6) == 0) { |
| // Hack for Theora video: Parse the "identification" hdr to get the "KFGSHIFT" parameter: |
| fIsTheora = True; |
| u_int8_t const KFGSHIFT = ((identificationHdr[40]&3)<<3) | (identificationHdr[41]>>5); |
| fGranuleIncrementPerFrame = (u_int64_t)(1 << KFGSHIFT); |
| } |
| OggFileSink::addData(identificationHdr, identificationHdrSize, presentationTime); |
| OggFileSink::addData(commentHdr, commentHdrSize, presentationTime); |
| |
| // Hack: Handle the "setup" header as if had arrived in the previous delivery, so it'll get |
| // written properly below: |
| if (setupHdrSize > fBufferSize) { |
| fAltFrameSize = fBufferSize; |
| fAltNumTruncatedBytes = setupHdrSize - fBufferSize; |
| } else { |
| fAltFrameSize = setupHdrSize; |
| fAltNumTruncatedBytes = 0; |
| } |
| memmove(fAltBuffer, setupHdr, fAltFrameSize); |
| fAltPresentationTime = presentationTime; |
| |
| delete[] identificationHdr; |
| delete[] commentHdr; |
| delete[] setupHdr; |
| } |
| } |
| |
| // Save this input frame for next time, and instead write the previous input frame now: |
| unsigned char* tmpPtr = fBuffer; fBuffer = fAltBuffer; fAltBuffer = tmpPtr; |
| unsigned prevFrameSize = fAltFrameSize; fAltFrameSize = frameSize; |
| unsigned prevNumTruncatedBytes = fAltNumTruncatedBytes; fAltNumTruncatedBytes = numTruncatedBytes; |
| struct timeval prevPresentationTime = fAltPresentationTime; fAltPresentationTime = presentationTime; |
| |
| // Call the parent class to complete the normal file write with the (previous) input frame: |
| FileSink::afterGettingFrame(prevFrameSize, prevNumTruncatedBytes, prevPresentationTime); |
| } |
| |
| void OggFileSink::ourOnSourceClosure(void* clientData) { |
| ((OggFileSink*)clientData)->ourOnSourceClosure(); |
| } |
| |
| void OggFileSink::ourOnSourceClosure() { |
| fHaveSeenEOF = True; |
| |
| // We still have the previously-arrived frame, so write it to the file before we end: |
| OggFileSink::addData(fAltBuffer, fAltFrameSize, fAltPresentationTime); |
| |
| // Handle the closure for real: |
| onSourceClosure(); |
| } |