OLD | NEW |
---|---|
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "media/filters/ffmpeg_video_decoder.h" | 5 #include "media/filters/ffmpeg_video_decoder.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/callback.h" | |
9 #include "base/message_loop.h" | 8 #include "base/message_loop.h" |
10 #include "base/task.h" | |
11 #include "media/base/demuxer_stream.h" | 9 #include "media/base/demuxer_stream.h" |
12 #include "media/base/filter_host.h" | 10 #include "media/base/filter_host.h" |
13 #include "media/base/limits.h" | 11 #include "media/base/limits.h" |
12 #include "media/base/video_decoder_config.h" | |
14 #include "media/base/video_frame.h" | 13 #include "media/base/video_frame.h" |
15 #include "media/ffmpeg/ffmpeg_common.h" | 14 #include "media/ffmpeg/ffmpeg_common.h" |
16 #include "media/video/ffmpeg_video_decode_engine.h" | 15 #include "media/video/ffmpeg_video_decode_engine.h" |
17 | 16 |
18 namespace media { | 17 namespace media { |
19 | 18 |
20 FFmpegVideoDecoder::FFmpegVideoDecoder(MessageLoop* message_loop) | 19 FFmpegVideoDecoder::FFmpegVideoDecoder(MessageLoop* message_loop) |
21 : message_loop_(message_loop), | 20 : message_loop_(message_loop), |
22 state_(kUnInitialized), | 21 state_(kUninitialized), |
23 decode_engine_(new FFmpegVideoDecodeEngine()) { | 22 decode_engine_(new FFmpegVideoDecodeEngine()) { |
24 } | 23 } |
25 | 24 |
26 FFmpegVideoDecoder::~FFmpegVideoDecoder() {} | 25 FFmpegVideoDecoder::~FFmpegVideoDecoder() {} |
27 | 26 |
28 void FFmpegVideoDecoder::Initialize(DemuxerStream* demuxer_stream, | 27 void FFmpegVideoDecoder::Initialize(DemuxerStream* demuxer_stream, |
29 const base::Closure& callback, | 28 const base::Closure& callback, |
30 const StatisticsCallback& stats_callback) { | 29 const StatisticsCallback& stats_callback) { |
31 if (MessageLoop::current() != message_loop_) { | 30 if (MessageLoop::current() != message_loop_) { |
32 message_loop_->PostTask( | 31 message_loop_->PostTask(FROM_HERE, base::Bind( |
33 FROM_HERE, | 32 &FFmpegVideoDecoder::Initialize, this, |
34 base::Bind(&FFmpegVideoDecoder::Initialize, this, | 33 make_scoped_refptr(demuxer_stream), callback, stats_callback)); |
35 make_scoped_refptr(demuxer_stream), | |
36 callback, stats_callback)); | |
37 return; | 34 return; |
38 } | 35 } |
39 | 36 |
40 DCHECK_EQ(MessageLoop::current(), message_loop_); | 37 DCHECK_EQ(MessageLoop::current(), message_loop_); |
41 DCHECK(!demuxer_stream_); | 38 DCHECK(!demuxer_stream_); |
42 DCHECK(initialize_callback_.is_null()); | |
43 | 39 |
44 if (!demuxer_stream) { | 40 if (!demuxer_stream) { |
45 host()->SetError(PIPELINE_ERROR_DECODE); | 41 host()->SetError(PIPELINE_ERROR_DECODE); |
46 callback.Run(); | 42 callback.Run(); |
47 return; | 43 return; |
48 } | 44 } |
49 | 45 |
50 demuxer_stream_ = demuxer_stream; | 46 demuxer_stream_ = demuxer_stream; |
51 initialize_callback_ = callback; | |
52 statistics_callback_ = stats_callback; | 47 statistics_callback_ = stats_callback; |
53 | 48 |
54 const VideoDecoderConfig& config = demuxer_stream->video_decoder_config(); | 49 const VideoDecoderConfig& config = demuxer_stream->video_decoder_config(); |
55 | 50 |
56 pts_stream_.Initialize(GetFrameDuration(config)); | 51 pts_stream_.Initialize(GetFrameDuration(config)); |
57 | 52 |
58 natural_size_ = config.natural_size(); | 53 natural_size_ = config.natural_size(); |
59 if (natural_size_.width() > Limits::kMaxDimension || | 54 if (natural_size_.width() > Limits::kMaxDimension || |
60 natural_size_.height() > Limits::kMaxDimension || | 55 natural_size_.height() > Limits::kMaxDimension || |
61 natural_size_.GetArea() > Limits::kMaxCanvas) { | 56 natural_size_.GetArea() > Limits::kMaxCanvas) { |
62 OnInitializeComplete(false); | 57 host()->SetError(PIPELINE_ERROR_DECODE); |
58 callback.Run(); | |
63 return; | 59 return; |
64 } | 60 } |
65 | 61 |
66 state_ = kInitializing; | 62 if (!decode_engine_->Initialize(config)) { |
67 decode_engine_->Initialize(this, config); | 63 host()->SetError(PIPELINE_ERROR_DECODE); |
68 } | 64 callback.Run(); |
65 return; | |
66 } | |
69 | 67 |
70 void FFmpegVideoDecoder::OnInitializeComplete(bool success) { | 68 state_ = kNormal; |
71 DCHECK_EQ(MessageLoop::current(), message_loop_); | 69 callback.Run(); |
72 DCHECK(!initialize_callback_.is_null()); | |
73 | |
74 if (success) { | |
75 state_ = kNormal; | |
76 } else { | |
77 host()->SetError(PIPELINE_ERROR_DECODE); | |
78 } | |
79 ResetAndRunCB(&initialize_callback_); | |
80 } | 70 } |
81 | 71 |
82 void FFmpegVideoDecoder::Stop(const base::Closure& callback) { | 72 void FFmpegVideoDecoder::Stop(const base::Closure& callback) { |
83 if (MessageLoop::current() != message_loop_) { | 73 if (MessageLoop::current() != message_loop_) { |
84 message_loop_->PostTask(FROM_HERE, base::Bind( | 74 message_loop_->PostTask(FROM_HERE, base::Bind( |
85 &FFmpegVideoDecoder::Stop, this, callback)); | 75 &FFmpegVideoDecoder::Stop, this, callback)); |
86 return; | 76 return; |
87 } | 77 } |
88 | 78 |
89 DCHECK_EQ(MessageLoop::current(), message_loop_); | 79 DCHECK_EQ(MessageLoop::current(), message_loop_); |
90 DCHECK(uninitialize_callback_.is_null()); | |
91 | 80 |
92 uninitialize_callback_ = callback; | 81 decode_engine_->Uninitialize(); |
93 if (state_ != kUnInitialized) | 82 state_ = kUninitialized; |
94 decode_engine_->Uninitialize(); | 83 callback.Run(); |
95 else | |
96 OnUninitializeComplete(); | |
97 } | 84 } |
98 | 85 |
99 void FFmpegVideoDecoder::OnUninitializeComplete() { | 86 void FFmpegVideoDecoder::Seek(base::TimeDelta time, const FilterStatusCB& cb) { |
87 if (MessageLoop::current() != message_loop_) { | |
88 message_loop_->PostTask(FROM_HERE, base::Bind( | |
89 &FFmpegVideoDecoder::Seek, this, time, cb)); | |
90 return; | |
91 } | |
92 | |
100 DCHECK_EQ(MessageLoop::current(), message_loop_); | 93 DCHECK_EQ(MessageLoop::current(), message_loop_); |
101 DCHECK(!uninitialize_callback_.is_null()); | |
102 | 94 |
103 state_ = kStopped; | 95 pts_stream_.Seek(time); |
104 | 96 cb.Run(PIPELINE_OK); |
105 ResetAndRunCB(&uninitialize_callback_); | |
106 } | 97 } |
107 | 98 |
108 void FFmpegVideoDecoder::Pause(const base::Closure& callback) { | 99 void FFmpegVideoDecoder::Pause(const base::Closure& callback) { |
109 if (MessageLoop::current() != message_loop_) { | 100 if (MessageLoop::current() != message_loop_) { |
110 message_loop_->PostTask(FROM_HERE, base::Bind( | 101 message_loop_->PostTask(FROM_HERE, base::Bind( |
111 &FFmpegVideoDecoder::Pause, this, callback)); | 102 &FFmpegVideoDecoder::Pause, this, callback)); |
112 return; | 103 return; |
113 } | 104 } |
114 | 105 |
115 state_ = kPausing; | |
116 callback.Run(); | 106 callback.Run(); |
117 } | 107 } |
118 | 108 |
119 void FFmpegVideoDecoder::Flush(const base::Closure& callback) { | 109 void FFmpegVideoDecoder::Flush(const base::Closure& callback) { |
120 if (MessageLoop::current() != message_loop_) { | 110 if (MessageLoop::current() != message_loop_) { |
121 message_loop_->PostTask(FROM_HERE, base::Bind( | 111 message_loop_->PostTask(FROM_HERE, base::Bind( |
122 &FFmpegVideoDecoder::Flush, this, callback)); | 112 &FFmpegVideoDecoder::Flush, this, callback)); |
123 return; | 113 return; |
124 } | 114 } |
125 | 115 |
126 DCHECK_EQ(MessageLoop::current(), message_loop_); | 116 DCHECK_EQ(MessageLoop::current(), message_loop_); |
127 DCHECK(flush_callback_.is_null()); | |
128 | |
129 state_ = kFlushing; | |
130 | |
131 FlushBuffers(); | |
132 | |
133 flush_callback_ = callback; | |
134 | 117 |
135 decode_engine_->Flush(); | 118 decode_engine_->Flush(); |
119 pts_stream_.Flush(); | |
120 state_ = kNormal; | |
121 callback.Run(); | |
136 } | 122 } |
137 | 123 |
138 void FFmpegVideoDecoder::OnFlushComplete() { | 124 void FFmpegVideoDecoder::DoDecodeBuffer(const scoped_refptr<Buffer>& buffer) { |
Ami GONE FROM CHROMIUM
2011/11/01 22:17:40
Why have this be a separate function? It makes De
scherkus (not reviewing)
2011/11/03 04:55:59
Done.
| |
139 DCHECK_EQ(MessageLoop::current(), message_loop_); | 125 DCHECK_EQ(MessageLoop::current(), message_loop_); |
140 DCHECK(!flush_callback_.is_null()); | 126 DCHECK_NE(state_, kUninitialized); |
141 | 127 DCHECK_NE(state_, kDecodeFinished); |
142 // Everything in the presentation time queue is invalid, clear the queue. | 128 DCHECK(!frame_ready_cb_.is_null()); |
143 pts_stream_.Flush(); | |
144 | |
145 // Mark flush operation had been done. | |
146 state_ = kNormal; | |
147 | |
148 ResetAndRunCB(&flush_callback_); | |
149 } | |
150 | |
151 void FFmpegVideoDecoder::Seek(base::TimeDelta time, const FilterStatusCB& cb) { | |
152 if (MessageLoop::current() != message_loop_) { | |
153 message_loop_->PostTask(FROM_HERE, | |
154 base::Bind(&FFmpegVideoDecoder::Seek, this, | |
155 time, cb)); | |
156 return; | |
157 } | |
158 | |
159 DCHECK_EQ(MessageLoop::current(), message_loop_); | |
160 DCHECK(seek_cb_.is_null()); | |
161 | |
162 pts_stream_.Seek(time); | |
163 seek_cb_ = cb; | |
164 decode_engine_->Seek(); | |
165 } | |
166 | |
167 void FFmpegVideoDecoder::OnSeekComplete() { | |
168 DCHECK_EQ(MessageLoop::current(), message_loop_); | |
169 DCHECK(!seek_cb_.is_null()); | |
170 | |
171 ResetAndRunCB(&seek_cb_, PIPELINE_OK); | |
172 } | |
173 | |
174 void FFmpegVideoDecoder::OnError() { | |
175 VideoFrameReady(NULL); | |
176 } | |
177 | |
178 void FFmpegVideoDecoder::OnReadComplete(Buffer* buffer_in) { | |
179 scoped_refptr<Buffer> buffer(buffer_in); | |
180 message_loop_->PostTask(FROM_HERE, base::Bind( | |
181 &FFmpegVideoDecoder::OnReadCompleteTask, this, buffer)); | |
182 } | |
183 | |
184 void FFmpegVideoDecoder::OnReadCompleteTask(scoped_refptr<Buffer> buffer) { | |
185 DCHECK_EQ(MessageLoop::current(), message_loop_); | |
186 DCHECK_NE(state_, kStopped); // because of Flush() before Stop(). | |
187 | 129 |
188 // During decode, because reads are issued asynchronously, it is possible to | 130 // During decode, because reads are issued asynchronously, it is possible to |
189 // receive multiple end of stream buffers since each read is acked. When the | 131 // receive multiple end of stream buffers since each read is acked. When the |
190 // first end of stream buffer is read, FFmpeg may still have frames queued | 132 // first end of stream buffer is read, FFmpeg may still have frames queued |
191 // up in the decoder so we need to go through the decode loop until it stops | 133 // up in the decoder so we need to go through the decode loop until it stops |
192 // giving sensible data. After that, the decoder should output empty | 134 // giving sensible data. After that, the decoder should output empty |
193 // frames. There are three states the decoder can be in: | 135 // frames. There are three states the decoder can be in: |
194 // | 136 // |
195 // kNormal: This is the starting state. Buffers are decoded. Decode errors | 137 // kNormal: This is the starting state. Buffers are decoded. Decode errors |
196 // are discarded. | 138 // are discarded. |
197 // kFlushCodec: There isn't any more input data. Call avcodec_decode_video2 | 139 // kFlushCodec: There isn't any more input data. Call avcodec_decode_video2 |
198 // until no more data is returned to flush out remaining | 140 // until no more data is returned to flush out remaining |
199 // frames. The input buffer is ignored at this point. | 141 // frames. The input buffer is ignored at this point. |
200 // kDecodeFinished: All calls return empty frames. | 142 // kDecodeFinished: All calls return empty frames. |
201 // | 143 // |
202 // These are the possible state transitions. | 144 // These are the possible state transitions. |
203 // | 145 // |
204 // kNormal -> kFlushCodec: | 146 // kNormal -> kFlushCodec: |
205 // When buffer->IsEndOfStream() is first true. | 147 // When buffer->IsEndOfStream() is first true. |
206 // kNormal -> kDecodeFinished: | 148 // kNormal -> kDecodeFinished: |
207 // A catastrophic failure occurs, and decoding needs to stop. | 149 // A catastrophic failure occurs, and decoding needs to stop. |
208 // kFlushCodec -> kDecodeFinished: | 150 // kFlushCodec -> kDecodeFinished: |
209 // When avcodec_decode_video2() returns 0 data or errors out. | 151 // When avcodec_decode_video2() returns 0 data or errors out. |
210 // (any state) -> kNormal: | 152 // (any state) -> kNormal: |
211 // Any time buffer->IsDiscontinuous() is true. | 153 // Any time Flush() is called. |
Ami GONE FROM CHROMIUM
2011/11/01 22:17:40
I'm surprised a Flush() can recover from a "catast
scherkus (not reviewing)
2011/11/03 04:55:59
Done.
| |
212 | 154 |
213 // Transition to kFlushCodec on the first end of stream buffer. | 155 // Transition to kFlushCodec on the first end of stream buffer. |
214 if (state_ == kNormal && buffer->IsEndOfStream()) { | 156 if (state_ == kNormal && buffer->IsEndOfStream()) { |
215 state_ = kFlushCodec; | 157 state_ = kFlushCodec; |
216 } | 158 } |
217 | 159 |
218 // Push all incoming timestamps into the priority queue as long as we have | 160 // Push all incoming timestamps into the priority queue as long as we have |
219 // not yet received an end of stream buffer. It is important that this line | 161 // not yet received an end of stream buffer. It is important that this line |
220 // stay below the state transition into kFlushCodec done above. | 162 // stay below the state transition into kFlushCodec done above. |
221 if (state_ == kNormal) { | 163 if (state_ == kNormal) { |
222 pts_stream_.EnqueuePts(buffer.get()); | 164 pts_stream_.EnqueuePts(buffer.get()); |
223 } | 165 } |
224 | 166 |
225 // Otherwise, attempt to decode a single frame. | 167 scoped_refptr<VideoFrame> video_frame; |
226 decode_engine_->ConsumeVideoSample(buffer); | 168 if (!decode_engine_->Decode(buffer, &video_frame)) { |
169 state_ = kDecodeFinished; | |
170 host()->SetError(PIPELINE_ERROR_DECODE); | |
171 return; | |
172 } | |
173 | |
174 // Any successful decode counts! | |
175 if (buffer->GetDataSize()) { | |
176 PipelineStatistics statistics; | |
177 statistics.video_bytes_decoded = buffer->GetDataSize(); | |
178 statistics_callback_.Run(statistics); | |
179 } | |
180 | |
181 // If we didn't get a frame then we've either completely finished decoding or | |
182 // we need more data. | |
183 if (!video_frame) { | |
184 if (state_ == kFlushCodec) { | |
185 state_ = kDecodeFinished; | |
186 DeliverFrame(VideoFrame::CreateEmptyFrame()); | |
187 return; | |
188 } | |
189 | |
190 ReadFromDemuxerStream(); | |
191 return; | |
192 } | |
193 | |
194 // If we got a frame make sure its timestamp is correct before sending it off. | |
195 pts_stream_.UpdatePtsAndDuration(video_frame.get()); | |
196 video_frame->SetTimestamp(pts_stream_.current_pts()); | |
197 video_frame->SetDuration(pts_stream_.current_duration()); | |
198 | |
199 DeliverFrame(video_frame); | |
227 } | 200 } |
228 | 201 |
229 void FFmpegVideoDecoder::ProduceVideoFrame( | 202 void FFmpegVideoDecoder::Read(const FrameReadyCB& callback) { |
230 scoped_refptr<VideoFrame> video_frame) { | |
231 if (MessageLoop::current() != message_loop_) { | 203 if (MessageLoop::current() != message_loop_) { |
232 if (state_ != kStopped) { | 204 message_loop_->PostTask(FROM_HERE, base::Bind( |
233 message_loop_->PostTask(FROM_HERE, base::Bind( | 205 &FFmpegVideoDecoder::Read, this, callback)); |
234 &FFmpegVideoDecoder::ProduceVideoFrame, this, video_frame)); | |
235 } | |
236 return; | 206 return; |
237 } | 207 } |
238 | 208 |
239 DCHECK_EQ(MessageLoop::current(), message_loop_); | 209 DCHECK_EQ(MessageLoop::current(), message_loop_); |
210 CHECK(!callback.is_null()); | |
Ami GONE FROM CHROMIUM
2011/11/01 22:17:40
Here and below s/CHECK/DCHECK/?
scherkus (not reviewing)
2011/11/03 04:55:59
maybe... I feel like this is programmer error and
| |
211 CHECK(frame_ready_cb_.is_null()) << "Overlapping decodes are not supported."; | |
240 | 212 |
241 // Synchronized flushing before stop should prevent this. | 213 // This can happen during shutdown after Stop() has been called. |
242 DCHECK_NE(state_, kStopped); | 214 if (state_ == kUninitialized) { |
243 | 215 // XXXXXXX |
244 // If the decoding is finished, we just always return empty frames. | 216 // just drop the callback? do we want to honour every callback no matter |
245 if (state_ == kDecodeFinished) { | 217 // what? |
scherkus (not reviewing)
2011/11/01 04:18:26
FYI
acolwell GONE FROM CHROMIUM
2011/11/01 18:31:03
Will the caller ever block progress if it doesn't
Ami GONE FROM CHROMIUM
2011/11/01 22:17:40
I say drop the callback.
scherkus (not reviewing)
2011/11/03 04:55:59
Done.
| |
246 // Signal VideoRenderer the end of the stream event. | 218 return; |
247 VideoFrameReady(VideoFrame::CreateEmptyFrame()); | |
248 | |
249 // Fall through, because we still need to keep record of this frame. | |
250 } | 219 } |
251 | 220 |
252 // Notify decode engine the available of new frame. | 221 // Return empty frames if decoding has finished. |
253 decode_engine_->ProduceVideoFrame(video_frame); | 222 if (state_ == kDecodeFinished) { |
254 } | 223 callback.Run(VideoFrame::CreateEmptyFrame()); |
224 return; | |
225 } | |
255 | 226 |
256 void FFmpegVideoDecoder::ConsumeVideoFrame( | 227 frame_ready_cb_ = callback; |
257 scoped_refptr<VideoFrame> video_frame, | 228 ReadFromDemuxerStream(); |
258 const PipelineStatistics& statistics) { | |
259 DCHECK_EQ(MessageLoop::current(), message_loop_); | |
260 DCHECK_NE(state_, kStopped); | |
261 | |
262 statistics_callback_.Run(statistics); | |
263 | |
264 if (video_frame.get()) { | |
265 if (kPausing == state_ || kFlushing == state_) { | |
266 frame_queue_flushed_.push_back(video_frame); | |
267 if (kFlushing == state_) | |
268 FlushBuffers(); | |
269 return; | |
270 } | |
271 | |
272 // If we actually got data back, enqueue a frame. | |
273 pts_stream_.UpdatePtsAndDuration(video_frame.get()); | |
274 | |
275 video_frame->SetTimestamp(pts_stream_.current_pts()); | |
276 video_frame->SetDuration(pts_stream_.current_duration()); | |
277 | |
278 VideoFrameReady(video_frame); | |
279 } else { | |
280 // When in kFlushCodec, any errored decode, or a 0-lengthed frame, | |
281 // is taken as a signal to stop decoding. | |
282 if (state_ == kFlushCodec) { | |
283 state_ = kDecodeFinished; | |
284 | |
285 // Signal VideoRenderer the end of the stream event. | |
286 VideoFrameReady(VideoFrame::CreateEmptyFrame()); | |
287 } | |
288 } | |
289 } | |
290 | |
291 void FFmpegVideoDecoder::ProduceVideoSample( | |
292 scoped_refptr<Buffer> buffer) { | |
293 DCHECK_EQ(MessageLoop::current(), message_loop_); | |
294 DCHECK_NE(state_, kStopped); | |
295 | |
296 demuxer_stream_->Read(base::Bind(&FFmpegVideoDecoder::OnReadComplete, | |
297 this)); | |
298 } | 229 } |
299 | 230 |
300 gfx::Size FFmpegVideoDecoder::natural_size() { | 231 gfx::Size FFmpegVideoDecoder::natural_size() { |
301 return natural_size_; | 232 return natural_size_; |
302 } | 233 } |
303 | 234 |
304 void FFmpegVideoDecoder::FlushBuffers() { | 235 void FFmpegVideoDecoder::ReadFromDemuxerStream() { |
305 while (!frame_queue_flushed_.empty()) { | 236 DCHECK_NE(state_, kUninitialized); |
306 scoped_refptr<VideoFrame> video_frame; | 237 DCHECK_NE(state_, kDecodeFinished); |
307 video_frame = frame_queue_flushed_.front(); | 238 DCHECK(!frame_ready_cb_.is_null()); |
308 frame_queue_flushed_.pop_front(); | |
309 | 239 |
310 // Return frames back to the decode engine. | 240 demuxer_stream_->Read(base::Bind(&FFmpegVideoDecoder::DecodeBuffer, this)); |
311 decode_engine_->ProduceVideoFrame(video_frame); | |
312 } | |
313 } | 241 } |
314 | 242 |
315 void FFmpegVideoDecoder::SetVideoDecodeEngineForTest( | 243 void FFmpegVideoDecoder::DecodeBuffer(Buffer* buffer) { |
316 VideoDecodeEngine* engine) { | 244 // TODO(scherkus): change DemuxerStream::Read() to use scoped_refptr<> for |
acolwell GONE FROM CHROMIUM
2011/11/01 18:31:03
This TODO should be in demuxer_stream.h
scherkus (not reviewing)
2011/11/03 04:55:59
TODO was fixed in r108178
| |
317 decode_engine_.reset(engine); | 245 // callback. |
246 message_loop_->PostTask(FROM_HERE, base::Bind( | |
247 &FFmpegVideoDecoder::DoDecodeBuffer, this, make_scoped_refptr(buffer))); | |
248 } | |
249 | |
250 void FFmpegVideoDecoder::DeliverFrame( | |
251 const scoped_refptr<VideoFrame>& video_frame) { | |
252 // Reset the callback before running to protect against reentrancy. | |
253 FrameReadyCB frame_ready_cb = frame_ready_cb_; | |
254 frame_ready_cb_.Reset(); | |
255 frame_ready_cb.Run(video_frame); | |
318 } | 256 } |
319 | 257 |
320 } // namespace media | 258 } // namespace media |
OLD | NEW |