Live555学习之(六)---------- 在Live555中实现录像

来源:互联网 发布:titan5知乎专栏 编辑:程序博客网 时间:2024/06/07 00:48

  Live555还提供了录像的示例程序,在testProgs目录下的playCommon.cpp中,Live555录像的基本原理就是创建一个RTSPClient去请求指定rtsp地址的视频,然后保存到文件里。

  playCommon.cpp打开一看就发现首先是各种全局函数的声明,然后是各种全局变量的声明,然后是main函数和各个函数的实现。main函数中首先还是创建TaskScheduler对象和UsageEnvironment对象,然后根据各种输入参数设置各种全局变量,最后就是创建一个RTSPClient对象请求指定rtsp地址的视频。

复制代码
 1 int main(int argc, char** argv)  2 { 3   // Begin by setting up our usage environment: 4   TaskScheduler* scheduler = BasicTaskScheduler::createNew(); 5   env = BasicUsageEnvironment::createNew(*scheduler); 6    7    /* 8         处理各种输入参数,在此省略 9    */10   streamURL = argv[1];11 12   // Create (or arrange to create) our client object:13   if (createHandlerServerForREGISTERCommand) {14     handlerServerForREGISTERCommand15       = HandlerServerForREGISTERCommand::createNew(*env, continueAfterClientCreation0,16                            handlerServerForREGISTERCommandPortNum, authDBForREGISTER,17                            verbosityLevel, progName);18     if (handlerServerForREGISTERCommand == NULL) {19       *env << "Failed to create a server for handling incoming \"REGISTER\" commands: " << env->getResultMsg() << "\n";20     } else {21       *env << "Awaiting an incoming \"REGISTER\" command on port " << handlerServerForREGISTERCommand->serverPortNum() << "\n";22     }23   } else {24     ourClient = createClient(*env, streamURL, verbosityLevel, progName);25     if (ourClient == NULL) {26       *env << "Failed to create " << clientProtocolName << " client: " << env->getResultMsg() << "\n";27       shutdown();28     }29     continueAfterClientCreation1();30   }31 32   // All subsequent activity takes place within the event loop:33   env->taskScheduler().doEventLoop(); // does not return34 35   return 0; // only to prevent compiler warning36 }37   
复制代码

   createClient函数在openRTSP.cpp文件中定义,内容很简单,就是调用了RTSPClient::createNew函数创建了一个RTSPClient对象。我们来看continueAfterClientCreation1函数

复制代码
  1 void continueAfterClientCreation1() {  2   setUserAgentString(userAgent);  3   4   if (sendOptionsRequest) {  5     // Begin by sending an "OPTIONS" command:  6     getOptions(continueAfterOPTIONS);                  // 发送OPTIONS命令,我们也可以跳过这一步直接发送DESCRIBE命令  7   } else {  8     continueAfterOPTIONS(NULL, 0, NULL);  9   } 10 } 11  12 void getOptions(RTSPClient::responseHandler* afterFunc) {  13   ourRTSPClient->sendOptionsCommand(afterFunc, ourAuthenticator); 14 } 15  16 void continueAfterOPTIONS(RTSPClient*, int resultCode, char* resultString) { 17   if (sendOptionsRequestOnly) { 18     if (resultCode != 0) { 19       *env << clientProtocolName << " \"OPTIONS\" request failed: " << resultString << "\n"; 20     } else { 21       *env << clientProtocolName << " \"OPTIONS\" request returned: " << resultString << "\n"; 22     } 23     shutdown(); 24   } 25   delete[] resultString; 26  27   // Next, get a SDP description for the stream: 28   getSDPDescription(continueAfterDESCRIBE);                     // 发送DESCRIBE命令 29 } 30  31 void getSDPDescription(RTSPClient::responseHandler* afterFunc) { 32   ourRTSPClient->sendDescribeCommand(afterFunc, ourAuthenticator); 33 } 34  35 void continueAfterDESCRIBE(RTSPClient*, int resultCode, char* resultString) { 36   if (resultCode != 0) { 37     *env << "Failed to get a SDP description for the URL \"" << streamURL << "\": " << resultString << "\n"; 38     delete[] resultString; 39     shutdown(); 40   } 41  42   char* sdpDescription = resultString; 43   *env << "Opened URL \"" << streamURL << "\", returning a SDP description:\n" << sdpDescription << "\n"; 44  45   // Create a media session object from this SDP description: 46   session = MediaSession::createNew(*env, sdpDescription);          //创建MediaSession 47   delete[] sdpDescription; 48   if (session == NULL) { 49     *env << "Failed to create a MediaSession object from the SDP description: " << env->getResultMsg() << "\n"; 50     shutdown(); 51   } else if (!session->hasSubsessions()) { 52     *env << "This session has no media subsessions (i.e., no \"m=\" lines)\n"; 53     shutdown(); 54   } 55  56   // Then, setup the "RTPSource"s for the session:             57   MediaSubsessionIterator iter(*session); 58   MediaSubsession *subsession; 59   Boolean madeProgress = False; 60   char const* singleMediumToTest = singleMedium; 61   while ((subsession = iter.next()) != NULL) { 62     // If we've asked to receive only a single medium, then check this now: 63     if (singleMediumToTest != NULL) { 64       if (strcmp(subsession->mediumName(), singleMediumToTest) != 0) { 65           *env << "Ignoring \"" << subsession->mediumName() 66               << "/" << subsession->codecName() 67               << "\" subsession, because we've asked to receive a single " << singleMedium 68               << " session only\n"; 69     continue; 70       } else { 71     // Receive this subsession only 72     singleMediumToTest = "xxxxx"; 73         // this hack ensures that we get only 1 subsession of this type 74       } 75     } 76  77     if (desiredPortNum != 0) { 78       subsession->setClientPortNum(desiredPortNum);                       //创建相关的RTPSource、Groupsock等资源 79       desiredPortNum += 2; 80     } 81  82     if (createReceivers) {                                                  //我们接收数据然后保存在文件中,createReceivers为true 83       if (!subsession->initiate(simpleRTPoffsetArg)) {                      //初始化MediaSubsession 84     *env << "Unable to create receiver for \"" << subsession->mediumName() 85          << "/" << subsession->codecName() 86          << "\" subsession: " << env->getResultMsg() << "\n"; 87       } else { 88     *env << "Created receiver for \"" << subsession->mediumName() 89          << "/" << subsession->codecName() << "\" subsession ("; 90     if (subsession->rtcpIsMuxed()) { 91       *env << "client port " << subsession->clientPortNum(); 92     } else { 93       *env << "client ports " << subsession->clientPortNum() 94            << "-" << subsession->clientPortNum()+1; 95     } 96     *env << ")\n"; 97     madeProgress = True; 98      99     if (subsession->rtpSource() != NULL) {100       // Because we're saving the incoming data, rather than playing101       // it in real time, allow an especially large time threshold102       // (1 second) for reordering misordered incoming packets:103       unsigned const thresh = 1000000; // 1 second104       subsession->rtpSource()->setPacketReorderingThresholdTime(thresh);105       106       // Set the RTP source's OS socket buffer size as appropriate - either if we were explicitly asked (using -B),107       // or if the desired FileSink buffer size happens to be larger than the current OS socket buffer size.108       // (The latter case is a heuristic, on the assumption that if the user asked for a large FileSink buffer size,109       // then the input data rate may be large enough to justify increasing the OS socket buffer size also.)110       int socketNum = subsession->rtpSource()->RTPgs()->socketNum();111       unsigned curBufferSize = getReceiveBufferSize(*env, socketNum);112       if (socketInputBufferSize > 0 || fileSinkBufferSize > curBufferSize) {113         unsigned newBufferSize = socketInputBufferSize > 0 ? socketInputBufferSize : fileSinkBufferSize;114         newBufferSize = setReceiveBufferTo(*env, socketNum, newBufferSize);115         if (socketInputBufferSize > 0) { // The user explicitly asked for the new socket buffer size; announce it:116           *env << "Changed socket receive buffer size for the \""117            << subsession->mediumName()118            << "/" << subsession->codecName()119            << "\" subsession from "120            << curBufferSize << " to "121            << newBufferSize << " bytes\n";122         }123       }124     }125       }126     } else {127       if (subsession->clientPortNum() == 0) {128     *env << "No client port was specified for the \""129          << subsession->mediumName()130          << "/" << subsession->codecName()131          << "\" subsession.  (Try adding the \"-p <portNum>\" option.)\n";132       } else {133         madeProgress = True;134       }135     }136   }137   if (!madeProgress) shutdown();138 139   // Perform additional 'setup' on each subsession, before playing them:140   setupStreams();                                    //对每个ServerMediaSubsession发送SETUP命令141 }
复制代码

  上面的流程和RTSPClient端与服务器建立连接的过程基本类似,先发送OPTIONS命令,然后发送DESCRIBE命令,然后发送SETUP命令。我们在此也可以忽略发送OPTIONS命令,直接从发送DESCRIBE命令开始。在setupStreams函数中分别对每个ServerMediaSubsession发送SETUP命令,我们来看一下setupStreams函数

复制代码
 1 void setupStreams() { 2   static MediaSubsessionIterator* setupIter = NULL; 3   if (setupIter == NULL) setupIter = new MediaSubsessionIterator(*session); 4   while ((subsession = setupIter->next()) != NULL) { 5     // We have another subsession left to set up: 6     if (subsession->clientPortNum() == 0) continue; // port # was not set 7  8     setupSubsession(subsession, streamUsingTCP, forceMulticastOnUnspecified, continueAfterSETUP);  //发送SETUP命令,建立与ServerMediaSubsession的连接 9     return;10   }11 12   // We're done setting up subsessions.  //与所有的ServerMediaSubsession建立连接成功13   delete setupIter;14   if (!madeProgress) shutdown();15 16   // Create output files:17   if (createReceivers) {18     if (fileOutputInterval > 0) {19       createPeriodicOutputFiles();    //创建周期性的输出文件,例如:我们可以设置每一个小时输出一个录像文件20     } else {21       createOutputFiles("");22     }23   }24 25   // Finally, start playing each subsession, to start the data flow:26   if (duration == 0) {27     if (scale > 0) duration = session->playEndTime() - initialSeekTime; // use SDP end time28     else if (scale < 0) duration = initialSeekTime;29   }30   if (duration < 0) duration = 0.0;31 32   endTime = initialSeekTime;33   if (scale > 0) {34     if (duration <= 0) endTime = -1.0f;35     else endTime = initialSeekTime + duration;36   } else {37     endTime = initialSeekTime - duration;38     if (endTime < 0) endTime = 0.0f;39   }40                 // 发送PLAY命令请求开始播放视频41   char const* absStartTime = initialAbsoluteSeekTime != NULL ? initialAbsoluteSeekTime : session->absStartTime();42   if (absStartTime != NULL) {43     // Either we or the server have specified that seeking should be done by 'absolute' time:44     startPlayingSession(session, absStartTime, session->absEndTime(), scale, continueAfterPLAY);45   } else {46     // Normal case: Seek by relative time (NPT):47     startPlayingSession(session, initialSeekTime, endTime, scale, continueAfterPLAY);        48   }49 }50 51 void setupSubsession(MediaSubsession* subsession, Boolean streamUsingTCP, Boolean forceMulticastOnUnspecified, RTSPClient::responseHandler* afterFunc) {52   53   ourRTSPClient->sendSetupCommand(*subsession, afterFunc, False, streamUsingTCP, forceMulticastOnUnspecified, ourAuthenticator);54 }55 56 void startPlayingSession(MediaSession* session, double start, double end, float scale, RTSPClient::responseHandler* afterFunc) {57   printf("\n\n\n%f - %f\n\n\n",start,end);58   ourRTSPClient->sendPlayCommand(*session, afterFunc, start, end, scale, ourAuthenticator);59 }
复制代码

  在setupStreams函数中依次与每个ServerMediaSubsession建立连接,都建立连接成功后,然后就调用createPeriodicOutputFiles函数周期性地创建输出录像的文件,然后开始发送PLAY命令请求播放视频。接下来我们看看createPeriodicOutputFiles这个函数

复制代码
  1 void createPeriodicOutputFiles() {  2   // Create a filename suffix that notes the time interval that's being recorded:  3   char periodicFileNameSuffix[100];  4   snprintf(periodicFileNameSuffix, sizeof periodicFileNameSuffix, "-%05d-%05d",  5        fileOutputSecondsSoFar, fileOutputSecondsSoFar + fileOutputInterval);  6   createOutputFiles(periodicFileNameSuffix);              //创建输出文件  7   8   // Schedule an event for writing the next output file:    //添加一个停止当前录像,创建新录像文件的任务  9   periodicFileOutputTask 10     = env->taskScheduler().scheduleDelayedTask(fileOutputInterval*1000000, 11                            (TaskFunc*)periodicFileOutputTimerHandler, 12                            (void*)NULL); 13 } 14  15 void createOutputFiles(char const* periodicFilenameSuffix) { 16   char outFileName[1000]; 17 
// 创建对应的FileSink来获取和保存视频数据 18 if (outputQuickTimeFile || outputAVIFile) { 19 if (periodicFilenameSuffix[0] == '\0') { 20 // Normally (unless the '-P <interval-in-seconds>' option was given) we output to 'stdout': 21 sprintf(outFileName, "stdout"); 22 } else { 23 // Otherwise output to a type-specific file name, containing "periodicFilenameSuffix": 24 char const* prefix = fileNamePrefix[0] == '\0' ? "output" : fileNamePrefix; 25 snprintf(outFileName, sizeof outFileName, "%s%s.%s", prefix, periodicFilenameSuffix, 26 outputAVIFile ? "avi" : generateMP4Format ? "mp4" : "mov"); 27 } 28 29 if (outputQuickTimeFile) { 30 qtOut = QuickTimeFileSink::createNew(*env, *session, outFileName, 31 fileSinkBufferSize, 32 movieWidth, movieHeight, 33 movieFPS, 34 packetLossCompensate, 35 syncStreams, 36 generateHintTracks, 37 generateMP4Format); 38 if (qtOut == NULL) { 39 *env << "Failed to create a \"QuickTimeFileSink\" for outputting to \"" 40 << outFileName << "\": " << env->getResultMsg() << "\n"; 41 shutdown(); 42 } else { 43 *env << "Outputting to the file: \"" << outFileName << "\"\n"; 44 } 45 46 qtOut->startPlaying(sessionAfterPlaying, NULL); 47 } else { // outputAVIFile 48 aviOut = AVIFileSink::createNew(*env, *session, outFileName, 49 fileSinkBufferSize, 50 movieWidth, movieHeight, 51 movieFPS, 52 packetLossCompensate); 53 if (aviOut == NULL) { 54 *env << "Failed to create an \"AVIFileSink\" for outputting to \"" 55 << outFileName << "\": " << env->getResultMsg() << "\n"; 56 shutdown(); 57 } else { 58 *env << "Outputting to the file: \"" << outFileName << "\"\n"; 59 } 60 61 aviOut->startPlaying(sessionAfterPlaying, NULL); 62 } 63 } else { //我直接保存录像成.H264文件,所以执行此else分支 64 // Create and start "FileSink"s for each subsession: 65 madeProgress = False; 66 MediaSubsessionIterator iter(*session); 67 while ((subsession = iter.next()) != NULL) { 68 if (subsession->readSource() == NULL) continue; // was not initiated 69 70 // Create an output file for each desired stream: 71 if (singleMedium == NULL || periodicFilenameSuffix[0] != '\0') { 72 // Output file name is 73 // "<filename-prefix><medium_name>-<codec_name>-<counter><periodicFilenameSuffix>" 74 static unsigned streamCounter = 0; 75 snprintf(outFileName, sizeof outFileName, "%s%s-%s-%d%s", 76 fileNamePrefix, subsession->mediumName(), 77 subsession->codecName(), ++streamCounter, periodicFilenameSuffix); 78 } else { 79 // When outputting a single medium only, we output to 'stdout 80 // (unless the '-P <interval-in-seconds>' option was given): 81 sprintf(outFileName, "stdout"); 82 } 83 84 FileSink* fileSink = NULL; 85 Boolean createOggFileSink = False; // by default 86 if (strcmp(subsession->mediumName(), "video") == 0) { 87 if (strcmp(subsession->codecName(), "H264") == 0) { // 创建H264VideoFileSink来获取和保存H264视频数据 88 // For H.264 video stream, we use a special sink that adds 'start codes', 89 // and (at the start) the SPS and PPS NAL units: 90 fileSink = H264VideoFileSink::createNew(*env, outFileName, 91 subsession->fmtp_spropparametersets(), 92 fileSinkBufferSize, oneFilePerFrame); 93 } else if (strcmp(subsession->codecName(), "H265") == 0) { 94 // For H.265 video stream, we use a special sink that adds 'start codes', 95 // and (at the start) the VPS, SPS, and PPS NAL units: 96 fileSink = H265VideoFileSink::createNew(*env, outFileName, 97 subsession->fmtp_spropvps(), 98 subsession->fmtp_spropsps(), 99 subsession->fmtp_sproppps(),100 fileSinkBufferSize, oneFilePerFrame);101 } else if (strcmp(subsession->codecName(), "THEORA") == 0) {102 createOggFileSink = True;103 }104 } else if (strcmp(subsession->mediumName(), "audio") == 0) {105 if (strcmp(subsession->codecName(), "AMR") == 0 ||106 strcmp(subsession->codecName(), "AMR-WB") == 0) {107 // For AMR audio streams, we use a special sink that inserts AMR frame hdrs:108 fileSink = AMRAudioFileSink::createNew(*env, outFileName,109 fileSinkBufferSize, oneFilePerFrame);110 } else if (strcmp(subsession->codecName(), "VORBIS") == 0 ||111 strcmp(subsession->codecName(), "OPUS") == 0) {112 createOggFileSink = True;113 }114 }115 if (createOggFileSink) {116 fileSink = OggFileSink117 ::createNew(*env, outFileName,118 subsession->rtpTimestampFrequency(), subsession->fmtp_config());119 } else if (fileSink == NULL) {120 // Normal case:                              //不属于上面的各种情形,创建一个普通FileSink获取和保存数据121 fileSink = FileSink::createNew(*env, outFileName,122 fileSinkBufferSize, oneFilePerFrame);123 }124 subsession->sink = fileSink;125 126 if (subsession->sink == NULL) {127 *env << "Failed to create FileSink for \"" << outFileName128 << "\": " << env->getResultMsg() << "\n";129 } else {130 if (singleMedium == NULL) {131 *env << "Created output file: \"" << outFileName << "\"\n";132 } else {133 *env << "Outputting data from the \"" << subsession->mediumName()134 << "/" << subsession->codecName()135 << "\" subsession to \"" << outFileName << "\"\n";136 }137 138 if (strcmp(subsession->mediumName(), "video") == 0 &&139 strcmp(subsession->codecName(), "MP4V-ES") == 0 &&140 subsession->fmtp_config() != NULL) {141 // For MPEG-4 video RTP streams, the 'config' information142 // from the SDP description contains useful VOL etc. headers.143 // Insert this data at the front of the output file:144 unsigned configLen;145 unsigned char* configData146 = parseGeneralConfigStr(subsession->fmtp_config(), configLen);147 struct timeval timeNow;148 gettimeofday(&timeNow, NULL);149 fileSink->addData(configData, configLen, timeNow);150 delete[] configData;151 }152 153 subsession->sink->startPlaying(*(subsession->readSource()), // 开始获取数据并保存154 subsessionAfterPlaying,155 subsession);156 157 // Also set a handler to be called if a RTCP "BYE" arrives158 // for this subsession:159 if (subsession->rtcpInstance() != NULL) {160 subsession->rtcpInstance()->setByeHandler(subsessionByeHandler, subsession);161 }162 163 madeProgress = True;164 }165 }166 if (!madeProgress) shutdown();167 }168 }169 170 void periodicFileOutputTimerHandler(void* /*clientData*/) { //完成了一个录像文件,关闭相关资源,开始下一个录像文件171 fileOutputSecondsSoFar += fileOutputInterval;172 173 // First, close the existing output files:174 closeMediaSinks();175 176 // Then, create new output files:177 createPeriodicOutputFiles();178 }179 180 void closeMediaSinks() { //关闭FileSink,释放资源181 Medium::close(qtOut); qtOut = NULL;182 Medium::close(aviOut); aviOut = NULL;183 184 if (session == NULL) return;185 MediaSubsessionIterator iter(*session);186 MediaSubsession* subsession;187 while ((subsession = iter.next()) != NULL) {188 Medium::close(subsession->sink);189 subsession->sink = NULL;190 }191 192 }
复制代码

  首先创建一个录像文件,然后创建FileSink从服务器获取和保存录像数据,当指定的录像时长到了,就终止当前录像,开始下一个录像文件。如此,就可以实现每隔指定的一段时间录像成一个文件的功能。我这里是获取的H264编码的视频,创建的是H264VideoFileSink来获取和保存数据,H264VideoFileSink是H264or5VideoFileSink的子类,H264or5VideoFileSink又是FileSink的子类。FileSink通过MediaSubsession的FramedSource获取数据,然后保存到文件中,我们来看一下保存文件的过程。

复制代码
 1 void H264or5VideoFileSink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime) { 2   unsigned char const start_code[4] = {0x00, 0x00, 0x00, 0x01};          //H264帧的开头是0x00000001 3  4   if (!fHaveWrittenFirstFrame) { 5     // If we have NAL units encoded in "sprop parameter strings", prepend these to the file: 6     for (unsigned j = 0; j < 3; ++j) { 7       unsigned numSPropRecords; 8       SPropRecord* sPropRecords 9     = parseSPropParameterSets(fSPropParameterSetsStr[j], numSPropRecords);10       for (unsigned i = 0; i < numSPropRecords; ++i) {11     addData(start_code, 4, presentationTime);12     addData(sPropRecords[i].sPropBytes, sPropRecords[i].sPropLength, presentationTime);13       }14       delete[] sPropRecords;15     }16     fHaveWrittenFirstFrame = True; // for next time17   }18 19   // Write the input data to the file, with the start code in front:20   addData(start_code, 4, presentationTime);              //先把每一帧的头部写入文件             21   //调用FileSink类的afterGettingFrame函数写入一帧的图像数据到文件22   // Call the parent class to complete the normal file write with the input data:23   FileSink::afterGettingFrame(frameSize, numTruncatedBytes, presentationTime);   24 }25 26 void FileSink::addData(unsigned char const* data, unsigned dataSize,27                struct timeval presentationTime) {28   if (fPerFrameFileNameBuffer != NULL && fOutFid == NULL) {29     // Special case: Open a new file on-the-fly for this frame30     if (presentationTime.tv_usec == fPrevPresentationTime.tv_usec &&31     presentationTime.tv_sec == fPrevPresentationTime.tv_sec) {32       // The presentation time is unchanged from the previous frame, so we add a 'counter'33       // suffix to the file name, to distinguish them:34       sprintf(fPerFrameFileNameBuffer, "%s-%lu.%06lu-%u", fPerFrameFileNamePrefix,35           presentationTime.tv_sec, presentationTime.tv_usec, ++fSamePresentationTimeCounter);36     } else {37       sprintf(fPerFrameFileNameBuffer, "%s-%lu.%06lu", fPerFrameFileNamePrefix,38           presentationTime.tv_sec, presentationTime.tv_usec);39       fPrevPresentationTime = presentationTime; // for next time40       fSamePresentationTimeCounter = 0; // for next time41     }42     fOutFid = OpenOutputFile(envir(), fPerFrameFileNameBuffer);43   }44 45   // Write to our file:46 #ifdef TEST_LOSS47   static unsigned const framesPerPacket = 10;48   static unsigned const frameCount = 0;49   static Boolean const packetIsLost;50   if ((frameCount++)%framesPerPacket == 0) {51     packetIsLost = (our_random()%10 == 0); // simulate 10% packet loss #####52   }53 54   if (!packetIsLost)55 #endif56   if (fOutFid != NULL && data != NULL) {57     fwrite(data, 1, dataSize, fOutFid);          // 写数据到文件58   }59 }60 61 62 void FileSink::afterGettingFrame(unsigned frameSize,63                  unsigned numTruncatedBytes,64                  struct timeval presentationTime) {65   if (numTruncatedBytes > 0) {66     envir() << "FileSink::afterGettingFrame(): The input frame data was too large for our buffer size ("67         << fBufferSize << ").  "68             << numTruncatedBytes << " bytes of trailing data was dropped!  Correct this by increasing the \"bufferSize\" parameter in the \"createNew()\" call to at least "69             << fBufferSize + numTruncatedBytes << "\n";70   }71   addData(fBuffer, frameSize, presentationTime);        // 写视频数据到文件72 73   if (fOutFid == NULL || fflush(fOutFid) == EOF) {74     // The output file has closed.  Handle this the same way as if the input source had closed:75     if (fSource != NULL) fSource->stopGettingFrames();76     onSourceClosure();77     return;78   }79 80   if (fPerFrameFileNameBuffer != NULL) {81     if (fOutFid != NULL) { fclose(fOutFid); fOutFid = NULL; }82   }83 84   // Then try getting the next frame:85   continuePlaying();                // 获取下一帧的数据86 }
复制代码

  H264VideoFileSink从FramedSource获取一帧数据后,先写H264帧的头部,然后再写视频数据,然后再获取下一帧数据,如此不断地将获取的视频数据写到录像文件中去。

原创粉丝点击