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 handlerServerForREGISTERCommand
15 = 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 return
34
35 return 0; // only to prevent compiler warning
36 }
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 playing
101 // it in real time, allow an especially large time threshold
102 // (1 second) for reordering misordered incoming packets:
103 unsigned const thresh = 1000000; // 1 second
104 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 time
28 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 = OggFileSink
117 ::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 \"" << outFileName
128 << "\": " << 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' information
142 // 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* configData
146 = 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" arrives
158 // 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 time
17 }
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 frame
30 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 time
40 fSamePresentationTimeCounter = 0; // for next time
41 }
42 fOutFid = OpenOutputFile(envir(), fPerFrameFileNameBuffer);
43 }
44
45 // Write to our file:
46 #ifdef TEST_LOSS
47 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 #endif
56 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帧的头部,然后再写视频数据,然后再获取下一帧数据,如此不断地将获取的视频数据写到录像文件中去。
作者:昨夜星辰