OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "media/cast/sender/h264_vt_encoder.h" | |
6 | |
7 #include <algorithm> | |
miu
2014/11/20 23:16:55
Don't think you need this anymore.
...but you do
jfroy
2014/11/20 23:38:30
I don't need algo anymore indeed, but also don't t
| |
8 #include <string> | |
9 | |
10 #include "base/big_endian.h" | |
11 #include "base/bind.h" | |
12 #include "base/bind_helpers.h" | |
13 #include "base/location.h" | |
14 #include "base/logging.h" | |
15 #include "media/base/mac/corevideo_glue.h" | |
16 #include "media/base/mac/video_frame_mac.h" | |
17 #include "media/cast/sender/video_frame_factory.h" | |
18 | |
19 namespace media { | |
20 namespace cast { | |
21 | |
22 namespace { | |
23 | |
24 // Container for the associated data of a video frame being processed. | |
25 struct InProgressFrameEncode { | |
26 const RtpTimestamp rtp_timestamp; | |
27 const base::TimeTicks reference_time; | |
28 const VideoEncoder::FrameEncodedCallback frame_encoded_callback; | |
29 | |
30 InProgressFrameEncode(RtpTimestamp rtp, | |
31 base::TimeTicks r_time, | |
32 VideoEncoder::FrameEncodedCallback callback) | |
33 : rtp_timestamp(rtp), | |
34 reference_time(r_time), | |
35 frame_encoded_callback(callback) {} | |
36 }; | |
37 | |
38 base::ScopedCFTypeRef<CFDictionaryRef> DictionaryWithKeyValue(CFTypeRef key, | |
39 CFTypeRef value) { | |
40 CFTypeRef keys[1] = {key}; | |
41 CFTypeRef values[1] = {value}; | |
42 return base::ScopedCFTypeRef<CFDictionaryRef>(CFDictionaryCreate( | |
43 kCFAllocatorDefault, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, | |
44 &kCFTypeDictionaryValueCallBacks)); | |
45 } | |
46 | |
47 template <typename NalSizeType> | |
48 void CopyNalsToAnnexB(char* avcc_buffer, | |
49 const size_t avcc_size, | |
50 std::string* annexb_buffer) { | |
51 COMPILE_ASSERT(sizeof(NalSizeType) == 1 || sizeof(NalSizeType) == 2 || | |
52 sizeof(NalSizeType) == 4, | |
53 "NAL size type has unsupported size"); | |
54 static const char startcode_3[3] = {0, 0, 1}; | |
55 DCHECK(avcc_buffer); | |
56 DCHECK(annexb_buffer); | |
57 size_t bytes_left = avcc_size; | |
58 while (bytes_left > 0) { | |
59 DCHECK_GT(bytes_left, sizeof(NalSizeType)); | |
60 NalSizeType nal_size; | |
61 base::ReadBigEndian(avcc_buffer, &nal_size); | |
62 bytes_left -= sizeof(NalSizeType); | |
63 avcc_buffer += sizeof(NalSizeType); | |
64 | |
65 DCHECK_GE(bytes_left, nal_size); | |
66 annexb_buffer->append(startcode_3, sizeof(startcode_3)); | |
67 annexb_buffer->append(avcc_buffer, nal_size); | |
68 bytes_left -= nal_size; | |
69 avcc_buffer += nal_size; | |
70 } | |
71 } | |
72 | |
73 // Copy a H.264 frame stored in a CM sample buffer to an Annex B buffer. Copies | |
74 // parameter sets for keyframes before the frame data as well. | |
75 void CopySampleBufferToAnnexBBuffer(CoreMediaGlue::CMSampleBufferRef sbuf, | |
76 std::string* annexb_buffer, | |
77 bool keyframe) { | |
78 // Perform two pass, one to figure out the total output size, and another to | |
79 // copy the data after having performed a single output allocation. Note that | |
80 // we'll allocate a bit more because we'll count 4 bytes instead of 3 for | |
81 // video NALs. | |
82 | |
83 OSStatus status; | |
84 | |
85 // Get the sample buffer's block buffer and format description. | |
86 auto bb = CoreMediaGlue::CMSampleBufferGetDataBuffer(sbuf); | |
87 DCHECK(bb); | |
88 auto fdesc = CoreMediaGlue::CMSampleBufferGetFormatDescription(sbuf); | |
89 DCHECK(fdesc); | |
90 | |
91 size_t bb_size = CoreMediaGlue::CMBlockBufferGetDataLength(bb); | |
92 size_t total_bytes = bb_size; | |
93 | |
94 size_t pset_count; | |
95 int nal_size_field_bytes; | |
96 status = CoreMediaGlue::CMVideoFormatDescriptionGetH264ParameterSetAtIndex( | |
97 fdesc, 0, nullptr, nullptr, &pset_count, &nal_size_field_bytes); | |
98 if (status == | |
99 CoreMediaGlue::kCMFormatDescriptionBridgeError_InvalidParameter) { | |
100 DLOG(WARNING) << " assuming 2 parameter sets and 4 bytes NAL length header"; | |
101 pset_count = 2; | |
102 nal_size_field_bytes = 4; | |
103 } else if (status != noErr) { | |
104 DLOG(ERROR) | |
105 << " CMVideoFormatDescriptionGetH264ParameterSetAtIndex failed: " | |
106 << status; | |
107 return; | |
108 } | |
109 | |
110 if (keyframe) { | |
111 const uint8_t* pset; | |
112 size_t pset_size; | |
113 for (size_t pset_i = 0; pset_i < pset_count; ++pset_i) { | |
114 status = | |
115 CoreMediaGlue::CMVideoFormatDescriptionGetH264ParameterSetAtIndex( | |
116 fdesc, pset_i, &pset, &pset_size, nullptr, nullptr); | |
117 if (status != noErr) { | |
118 DLOG(ERROR) | |
119 << " CMVideoFormatDescriptionGetH264ParameterSetAtIndex failed: " | |
120 << status; | |
121 return; | |
122 } | |
123 total_bytes += pset_size + nal_size_field_bytes; | |
124 } | |
125 } | |
126 | |
127 annexb_buffer->reserve(total_bytes); | |
128 | |
129 // Copy all parameter sets before keyframes. | |
130 if (keyframe) { | |
131 const uint8_t* pset; | |
132 size_t pset_size; | |
133 for (size_t pset_i = 0; pset_i < pset_count; ++pset_i) { | |
134 status = | |
135 CoreMediaGlue::CMVideoFormatDescriptionGetH264ParameterSetAtIndex( | |
136 fdesc, pset_i, &pset, &pset_size, nullptr, nullptr); | |
137 if (status != noErr) { | |
138 DLOG(ERROR) | |
139 << " CMVideoFormatDescriptionGetH264ParameterSetAtIndex failed: " | |
140 << status; | |
141 return; | |
142 } | |
143 static const char startcode_4[4] = {0, 0, 0, 1}; | |
144 annexb_buffer->append(startcode_4, sizeof(startcode_4)); | |
145 annexb_buffer->append(reinterpret_cast<const char*>(pset), pset_size); | |
146 } | |
147 } | |
148 | |
149 // Block buffers can be composed of non-contiguous chunks. For the sake of | |
150 // keeping this code simple, flatten non-contiguous block buffers. | |
151 base::ScopedCFTypeRef<CoreMediaGlue::CMBlockBufferRef> contiguous_bb( | |
152 bb, base::scoped_policy::RETAIN); | |
153 if (!CoreMediaGlue::CMBlockBufferIsRangeContiguous(bb, 0, 0)) { | |
154 contiguous_bb.reset(); | |
155 status = CoreMediaGlue::CMBlockBufferCreateContiguous( | |
156 kCFAllocatorDefault, bb, kCFAllocatorDefault, nullptr, 0, 0, 0, | |
157 contiguous_bb.InitializeInto()); | |
158 if (status != noErr) { | |
159 DLOG(ERROR) << " CMBlockBufferCreateContiguous failed: " << status; | |
160 return; | |
161 } | |
162 } | |
163 | |
164 // Copy all the NAL units. In the process convert them from AVCC format | |
165 // (length header) to AnnexB format (start code). | |
166 char* bb_data; | |
167 status = CoreMediaGlue::CMBlockBufferGetDataPointer(contiguous_bb, 0, nullptr, | |
168 nullptr, &bb_data); | |
169 if (status != noErr) { | |
170 DLOG(ERROR) << " CMBlockBufferGetDataPointer failed: " << status; | |
171 return; | |
172 } | |
173 | |
174 if (nal_size_field_bytes == 1) { | |
175 CopyNalsToAnnexB<uint8_t>(bb_data, bb_size, annexb_buffer); | |
176 } else if (nal_size_field_bytes == 2) { | |
177 CopyNalsToAnnexB<uint16_t>(bb_data, bb_size, annexb_buffer); | |
178 } else if (nal_size_field_bytes == 4) { | |
179 CopyNalsToAnnexB<uint32_t>(bb_data, bb_size, annexb_buffer); | |
180 } else { | |
181 NOTREACHED(); | |
182 } | |
183 } | |
184 | |
185 // Implementation of the VideoFrameFactory interface using |CVPixelBufferPool|. | |
186 class VideoFrameFactoryCVPixelBufferPoolImpl : public VideoFrameFactory { | |
187 public: | |
188 VideoFrameFactoryCVPixelBufferPoolImpl( | |
189 base::ScopedCFTypeRef<CVPixelBufferPoolRef> pool) | |
miu
2014/11/20 23:16:55
const base::ScopedCFTypeRef<CVPixelBufferPoolRef>&
| |
190 : pool_(pool) {} | |
191 | |
192 ~VideoFrameFactoryCVPixelBufferPoolImpl() override {} | |
193 | |
194 scoped_refptr<VideoFrame> CreateFrame(base::TimeDelta timestamp) override { | |
195 base::ScopedCFTypeRef<CVPixelBufferRef> buffer; | |
196 CHECK_EQ(kCVReturnSuccess, | |
197 CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool_, | |
198 buffer.InitializeInto())); | |
199 return VideoFrame::WrapCVPixelBuffer(buffer, timestamp); | |
200 } | |
201 | |
202 private: | |
203 base::ScopedCFTypeRef<CVPixelBufferPoolRef> pool_; | |
204 | |
205 DISALLOW_COPY_AND_ASSIGN(VideoFrameFactoryCVPixelBufferPoolImpl); | |
206 }; | |
207 | |
208 } // namespace | |
209 | |
210 H264VideoToolboxEncoder::H264VideoToolboxEncoder( | |
211 scoped_refptr<CastEnvironment> cast_environment, | |
212 const VideoSenderConfig& video_config, | |
213 const CastInitializationCallback& initialization_cb) | |
214 : cast_environment_(cast_environment), | |
215 videotoolbox_glue_(VideoToolboxGlue::Get()), | |
216 frame_id_(kStartFrameId), | |
217 encode_next_frame_as_keyframe_(false) { | |
218 DCHECK(!initialization_cb.is_null()); | |
219 CastInitializationStatus initialization_status; | |
220 if (videotoolbox_glue_) { | |
221 initialization_status = (Initialize(video_config)) | |
222 ? STATUS_VIDEO_INITIALIZED | |
223 : STATUS_INVALID_VIDEO_CONFIGURATION; | |
224 } else { | |
225 LOG(ERROR) << " VideoToolbox is not available"; | |
226 initialization_status = STATUS_HW_VIDEO_ENCODER_NOT_SUPPORTED; | |
227 } | |
228 cast_environment_->PostTask( | |
229 CastEnvironment::MAIN, FROM_HERE, | |
230 base::Bind(initialization_cb, initialization_status)); | |
231 } | |
232 | |
233 H264VideoToolboxEncoder::~H264VideoToolboxEncoder() { | |
234 Teardown(); | |
235 } | |
236 | |
237 bool H264VideoToolboxEncoder::Initialize( | |
238 const VideoSenderConfig& video_config) { | |
239 DCHECK(thread_checker_.CalledOnValidThread()); | |
240 DCHECK(!compression_session_); | |
241 | |
242 // Note that the encoder object is given to the compression session as the | |
243 // callback context using a raw pointer. The C API does not allow us to use | |
244 // a smart pointer, nor is this encoder ref counted. However, this is still | |
245 // safe, because we 1) we own the compression session and 2) we tear it down | |
246 // safely. When destructing the encoder, the compression session is flushed | |
247 // and invalidated. Internally, VideoToolbox will join all of its threads | |
248 // before returning to the client. Therefore, when control returns to us, we | |
249 // are guaranteed that the output callback will not execute again. | |
250 | |
251 // On OS X, allow the hardware encoder. Don't require it, it does not support | |
252 // all configurations (some of which are used for testing). | |
253 base::ScopedCFTypeRef<CFDictionaryRef> encoder_spec; | |
254 #if !defined(OS_IOS) | |
255 encoder_spec = DictionaryWithKeyValue( | |
256 videotoolbox_glue_ | |
257 ->kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder() , | |
258 kCFBooleanTrue); | |
259 #endif | |
260 | |
261 VTCompressionSessionRef session; | |
262 OSStatus status = videotoolbox_glue_->VTCompressionSessionCreate( | |
263 kCFAllocatorDefault, video_config.width, video_config.height, | |
264 CoreMediaGlue::kCMVideoCodecType_H264, encoder_spec, | |
265 nullptr /* sourceImageBufferAttributes */, | |
266 nullptr /* compressedDataAllocator */, | |
267 &H264VideoToolboxEncoder::CompressionCallback, | |
268 reinterpret_cast<void*>(this), &session); | |
269 if (status != noErr) { | |
270 DLOG(ERROR) << " VTCompressionSessionCreate failed: " << status; | |
271 return false; | |
272 } | |
273 compression_session_.reset(session); | |
274 | |
275 ConfigureSession(video_config); | |
276 | |
277 return true; | |
278 } | |
279 | |
280 void H264VideoToolboxEncoder::ConfigureSession( | |
281 const VideoSenderConfig& video_config) { | |
282 SetSessionProperty( | |
283 videotoolbox_glue_->kVTCompressionPropertyKey_ProfileLevel(), | |
284 videotoolbox_glue_->kVTProfileLevel_H264_Main_AutoLevel()); | |
285 SetSessionProperty(videotoolbox_glue_->kVTCompressionPropertyKey_RealTime(), | |
286 true); | |
287 SetSessionProperty( | |
288 videotoolbox_glue_->kVTCompressionPropertyKey_AllowFrameReordering(), | |
289 false); | |
290 SetSessionProperty( | |
291 videotoolbox_glue_->kVTCompressionPropertyKey_MaxKeyFrameInterval(), 240); | |
292 SetSessionProperty( | |
293 videotoolbox_glue_ | |
294 ->kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration(), | |
295 240); | |
296 // TODO(jfroy): implement better bitrate control | |
297 // https://crbug.com/425352 | |
298 SetSessionProperty( | |
299 videotoolbox_glue_->kVTCompressionPropertyKey_AverageBitRate(), | |
300 (video_config.min_bitrate + video_config.max_bitrate) / 2); | |
301 SetSessionProperty( | |
302 videotoolbox_glue_->kVTCompressionPropertyKey_ExpectedFrameRate(), | |
303 video_config.max_frame_rate); | |
304 SetSessionProperty( | |
305 videotoolbox_glue_->kVTCompressionPropertyKey_ColorPrimaries(), | |
306 kCVImageBufferColorPrimaries_ITU_R_709_2); | |
307 SetSessionProperty( | |
308 videotoolbox_glue_->kVTCompressionPropertyKey_TransferFunction(), | |
309 kCVImageBufferTransferFunction_ITU_R_709_2); | |
310 SetSessionProperty( | |
311 videotoolbox_glue_->kVTCompressionPropertyKey_YCbCrMatrix(), | |
312 kCVImageBufferYCbCrMatrix_ITU_R_709_2); | |
313 } | |
314 | |
315 void H264VideoToolboxEncoder::Teardown() { | |
316 DCHECK(thread_checker_.CalledOnValidThread()); | |
317 | |
318 // If the compression session exists, invalidate it. This blocks until all | |
319 // pending output callbacks have returned and any internal threads have | |
320 // joined, ensuring no output callback ever sees a dangling encoder pointer. | |
321 if (compression_session_) { | |
322 videotoolbox_glue_->VTCompressionSessionInvalidate(compression_session_); | |
323 compression_session_.reset(); | |
324 } | |
325 } | |
326 | |
327 bool H264VideoToolboxEncoder::EncodeVideoFrame( | |
328 const scoped_refptr<media::VideoFrame>& video_frame, | |
329 const base::TimeTicks& reference_time, | |
330 const FrameEncodedCallback& frame_encoded_callback) { | |
331 DCHECK(thread_checker_.CalledOnValidThread()); | |
332 DCHECK(!reference_time.is_null()); | |
333 | |
334 if (!compression_session_) { | |
335 DLOG(ERROR) << " compression session is null"; | |
336 return false; | |
337 } | |
338 | |
339 // Wrap the VideoFrame in a CVPixelBuffer. In all cases, no data will be | |
340 // copied. If the VideoFrame was created by this encoder's video frame | |
341 // factory, then the returned CVPixelBuffer will have been obtained from the | |
342 // compression session's pixel buffer pool. This will eliminate a copy of the | |
343 // frame into memory visible by the hardware encoder. The VideoFrame's | |
344 // lifetime is extended for the lifetime of the returned CVPixelBuffer. | |
345 auto pixel_buffer = media::WrapVideoFrameInCVPixelBuffer(*video_frame); | |
346 if (!pixel_buffer) { | |
347 return false; | |
348 } | |
349 | |
350 auto timestamp_cm = CoreMediaGlue::CMTimeMake( | |
351 (reference_time - base::TimeTicks()).InMicroseconds(), USEC_PER_SEC); | |
352 | |
353 scoped_ptr<InProgressFrameEncode> request(new InProgressFrameEncode( | |
354 TimeDeltaToRtpDelta(video_frame->timestamp(), kVideoFrequency), | |
355 reference_time, frame_encoded_callback)); | |
356 | |
357 base::ScopedCFTypeRef<CFDictionaryRef> frame_props; | |
358 if (encode_next_frame_as_keyframe_) { | |
359 frame_props = DictionaryWithKeyValue( | |
360 videotoolbox_glue_->kVTEncodeFrameOptionKey_ForceKeyFrame(), | |
361 kCFBooleanTrue); | |
362 encode_next_frame_as_keyframe_ = false; | |
363 } | |
364 | |
365 VTEncodeInfoFlags info; | |
366 OSStatus status = videotoolbox_glue_->VTCompressionSessionEncodeFrame( | |
367 compression_session_, pixel_buffer, timestamp_cm, | |
368 CoreMediaGlue::CMTime{0, 0, 0, 0}, frame_props, | |
369 reinterpret_cast<void*>(request.release()), &info); | |
370 if (status != noErr) { | |
371 DLOG(ERROR) << " VTCompressionSessionEncodeFrame failed: " << status; | |
372 return false; | |
373 } | |
374 if ((info & VideoToolboxGlue::kVTEncodeInfo_FrameDropped)) { | |
375 DLOG(ERROR) << " frame dropped"; | |
376 return false; | |
377 } | |
378 | |
379 return true; | |
380 } | |
381 | |
382 void H264VideoToolboxEncoder::SetBitRate(int new_bit_rate) { | |
383 DCHECK(thread_checker_.CalledOnValidThread()); | |
384 // VideoToolbox does not seem to support bitrate reconfiguration. | |
385 } | |
386 | |
387 void H264VideoToolboxEncoder::GenerateKeyFrame() { | |
388 DCHECK(thread_checker_.CalledOnValidThread()); | |
389 DCHECK(compression_session_); | |
390 | |
391 encode_next_frame_as_keyframe_ = true; | |
392 } | |
393 | |
394 void H264VideoToolboxEncoder::LatestFrameIdToReference(uint32 /*frame_id*/) { | |
395 // Not supported by VideoToolbox in any meaningful manner. | |
396 } | |
397 | |
398 scoped_ptr<VideoFrameFactory> | |
399 H264VideoToolboxEncoder::CreateVideoFrameFactory() { | |
400 base::ScopedCFTypeRef<CVPixelBufferPoolRef> pool( | |
401 videotoolbox_glue_->VTCompressionSessionGetPixelBufferPool( | |
402 compression_session_), | |
403 base::scoped_policy::RETAIN); | |
404 return scoped_ptr<VideoFrameFactory>( | |
405 new VideoFrameFactoryCVPixelBufferPoolImpl(pool)); | |
406 } | |
407 | |
408 bool H264VideoToolboxEncoder::SetSessionProperty(CFStringRef key, | |
409 int32_t value) { | |
410 base::ScopedCFTypeRef<CFNumberRef> cfvalue( | |
411 CFNumberCreate(nullptr, kCFNumberSInt32Type, &value)); | |
412 return videotoolbox_glue_->VTSessionSetProperty(compression_session_, key, | |
413 cfvalue) == noErr; | |
414 } | |
415 | |
416 bool H264VideoToolboxEncoder::SetSessionProperty(CFStringRef key, bool value) { | |
417 CFBooleanRef cfvalue = (value) ? kCFBooleanTrue : kCFBooleanFalse; | |
418 return videotoolbox_glue_->VTSessionSetProperty(compression_session_, key, | |
419 cfvalue) == noErr; | |
420 } | |
421 | |
422 bool H264VideoToolboxEncoder::SetSessionProperty(CFStringRef key, | |
423 CFStringRef value) { | |
424 return videotoolbox_glue_->VTSessionSetProperty(compression_session_, key, | |
425 value) == noErr; | |
426 } | |
427 | |
428 void H264VideoToolboxEncoder::CompressionCallback(void* encoder_opaque, | |
429 void* request_opaque, | |
430 OSStatus status, | |
431 VTEncodeInfoFlags info, | |
432 CMSampleBufferRef sbuf) { | |
433 if (status != noErr) { | |
434 DLOG(ERROR) << " encoding failed: " << status; | |
435 return; | |
436 } | |
437 if ((info & VideoToolboxGlue::kVTEncodeInfo_FrameDropped)) { | |
438 DVLOG(2) << " frame dropped"; | |
439 return; | |
440 } | |
441 | |
442 auto encoder = reinterpret_cast<H264VideoToolboxEncoder*>(encoder_opaque); | |
443 const scoped_ptr<InProgressFrameEncode> request( | |
444 reinterpret_cast<InProgressFrameEncode*>(request_opaque)); | |
445 auto sample_attachments = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex( | |
446 CoreMediaGlue::CMSampleBufferGetSampleAttachmentsArray(sbuf, true), 0)); | |
447 | |
448 // If the NotSync key is not present, it implies Sync, which indicates a | |
449 // keyframe (at least I think, VT documentation is, erm, sparse). Could | |
450 // alternatively use kCMSampleAttachmentKey_DependsOnOthers == false. | |
451 bool keyframe = | |
452 !CFDictionaryContainsKey(sample_attachments, | |
453 CoreMediaGlue::kCMSampleAttachmentKey_NotSync()); | |
454 | |
455 // Increment the encoder-scoped frame id and assign the new value to this | |
456 // frame. VideoToolbox calls the output callback serially, so this is safe. | |
457 uint32 frame_id = ++encoder->frame_id_; | |
458 | |
459 scoped_ptr<EncodedFrame> encoded_frame(new EncodedFrame()); | |
460 encoded_frame->frame_id = frame_id; | |
461 encoded_frame->reference_time = request->reference_time; | |
462 encoded_frame->rtp_timestamp = request->rtp_timestamp; | |
463 if (keyframe) { | |
464 encoded_frame->dependency = EncodedFrame::KEY; | |
465 encoded_frame->referenced_frame_id = frame_id; | |
466 } else { | |
467 encoded_frame->dependency = EncodedFrame::DEPENDENT; | |
468 // H.264 supports complex frame reference schemes (multiple reference | |
469 // frames, slice references, backward and forward references, etc). Cast | |
470 // doesn't support the concept of forward-referencing frame dependencies or | |
471 // multiple frame dependencies; so pretend that all frames are only | |
472 // decodable after their immediately preceding frame is decoded. This will | |
473 // ensure a Cast receiver only attempts to decode the frames sequentially | |
474 // and in order. Furthermore, the encoder is configured to never use forward | |
475 // references (see |kVTCompressionPropertyKey_AllowFrameReordering|). There | |
476 // is no way to prevent multiple reference frames. | |
477 encoded_frame->referenced_frame_id = frame_id - 1; | |
478 } | |
479 | |
480 CopySampleBufferToAnnexBBuffer(sbuf, &encoded_frame->data, keyframe); | |
481 | |
482 encoder->cast_environment_->PostTask( | |
483 CastEnvironment::MAIN, FROM_HERE, | |
484 base::Bind(request->frame_encoded_callback, | |
485 base::Passed(&encoded_frame))); | |
486 } | |
487 | |
488 } // namespace cast | |
489 } // namespace media | |
OLD | NEW |