/********** 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 .) 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. // '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(); }