OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 "webkit/plugins/ppapi/content_decryptor_delegate.h" | |
6 | |
7 #include "base/callback_helpers.h" | |
8 #include "base/debug/trace_event.h" | |
9 #include "base/message_loop/message_loop_proxy.h" | |
10 #include "base/safe_numerics.h" | |
11 #include "media/base/audio_buffer.h" | |
12 #include "media/base/audio_decoder_config.h" | |
13 #include "media/base/bind_to_loop.h" | |
14 #include "media/base/channel_layout.h" | |
15 #include "media/base/data_buffer.h" | |
16 #include "media/base/decoder_buffer.h" | |
17 #include "media/base/decrypt_config.h" | |
18 #include "media/base/video_decoder_config.h" | |
19 #include "media/base/video_frame.h" | |
20 #include "media/base/video_util.h" | |
21 #include "ppapi/shared_impl/scoped_pp_resource.h" | |
22 #include "ppapi/shared_impl/var.h" | |
23 #include "ppapi/shared_impl/var_tracker.h" | |
24 #include "ppapi/thunk/enter.h" | |
25 #include "ppapi/thunk/ppb_buffer_api.h" | |
26 #include "ui/gfx/rect.h" | |
27 #include "webkit/plugins/ppapi/ppb_buffer_impl.h" | |
28 | |
29 using ppapi::ArrayBufferVar; | |
30 using ppapi::PpapiGlobals; | |
31 using ppapi::ScopedPPResource; | |
32 using ppapi::StringVar; | |
33 using ppapi::thunk::EnterResourceNoLock; | |
34 using ppapi::thunk::PPB_Buffer_API; | |
35 | |
36 namespace webkit { | |
37 namespace ppapi { | |
38 | |
39 namespace { | |
40 | |
41 // Fills |resource| with a PPB_Buffer_Impl and copies |data| into the buffer | |
42 // resource. The |*resource|, if valid, will be in the ResourceTracker with a | |
43 // reference-count of 0. If |data| is NULL, sets |*resource| to NULL. Returns | |
44 // true upon success and false if any error happened. | |
45 bool MakeBufferResource(PP_Instance instance, | |
46 const uint8* data, uint32_t size, | |
47 scoped_refptr<PPB_Buffer_Impl>* resource) { | |
48 TRACE_EVENT0("eme", "ContentDecryptorDelegate - MakeBufferResource"); | |
49 DCHECK(resource); | |
50 | |
51 if (!data || !size) { | |
52 DCHECK(!data && !size); | |
53 resource = NULL; | |
54 return true; | |
55 } | |
56 | |
57 scoped_refptr<PPB_Buffer_Impl> buffer( | |
58 PPB_Buffer_Impl::CreateResource(instance, size)); | |
59 if (!buffer.get()) | |
60 return false; | |
61 | |
62 BufferAutoMapper mapper(buffer.get()); | |
63 if (!mapper.data() || mapper.size() < size) | |
64 return false; | |
65 memcpy(mapper.data(), data, size); | |
66 | |
67 *resource = buffer; | |
68 return true; | |
69 } | |
70 | |
71 // Copies the content of |str| into |array|. | |
72 // Returns true if copy succeeded. Returns false if copy failed, e.g. if the | |
73 // |array_size| is smaller than the |str| length. | |
74 template <uint32_t array_size> | |
75 bool CopyStringToArray(const std::string& str, uint8 (&array)[array_size]) { | |
76 if (array_size < str.size()) | |
77 return false; | |
78 | |
79 memcpy(array, str.data(), str.size()); | |
80 return true; | |
81 } | |
82 | |
83 // Fills the |block_info| with information from |encrypted_buffer|. | |
84 // | |
85 // Returns true if |block_info| is successfully filled. Returns false | |
86 // otherwise. | |
87 static bool MakeEncryptedBlockInfo( | |
88 const scoped_refptr<media::DecoderBuffer>& encrypted_buffer, | |
89 uint32_t request_id, | |
90 PP_EncryptedBlockInfo* block_info) { | |
91 // TODO(xhwang): Fix initialization of PP_EncryptedBlockInfo here and | |
92 // anywhere else. | |
93 memset(block_info, 0, sizeof(*block_info)); | |
94 block_info->tracking_info.request_id = request_id; | |
95 | |
96 // EOS buffers need a request ID and nothing more. | |
97 if (encrypted_buffer->end_of_stream()) | |
98 return true; | |
99 | |
100 DCHECK(encrypted_buffer->data_size()) | |
101 << "DecryptConfig is set on an empty buffer"; | |
102 | |
103 block_info->tracking_info.timestamp = | |
104 encrypted_buffer->timestamp().InMicroseconds(); | |
105 block_info->data_size = encrypted_buffer->data_size(); | |
106 | |
107 const media::DecryptConfig* decrypt_config = | |
108 encrypted_buffer->decrypt_config(); | |
109 block_info->data_offset = decrypt_config->data_offset(); | |
110 | |
111 if (!CopyStringToArray(decrypt_config->key_id(), block_info->key_id) || | |
112 !CopyStringToArray(decrypt_config->iv(), block_info->iv)) | |
113 return false; | |
114 | |
115 block_info->key_id_size = decrypt_config->key_id().size(); | |
116 block_info->iv_size = decrypt_config->iv().size(); | |
117 | |
118 if (decrypt_config->subsamples().size() > arraysize(block_info->subsamples)) | |
119 return false; | |
120 | |
121 block_info->num_subsamples = decrypt_config->subsamples().size(); | |
122 for (uint32_t i = 0; i < block_info->num_subsamples; ++i) { | |
123 block_info->subsamples[i].clear_bytes = | |
124 decrypt_config->subsamples()[i].clear_bytes; | |
125 block_info->subsamples[i].cipher_bytes = | |
126 decrypt_config->subsamples()[i].cypher_bytes; | |
127 } | |
128 | |
129 return true; | |
130 } | |
131 | |
132 PP_AudioCodec MediaAudioCodecToPpAudioCodec(media::AudioCodec codec) { | |
133 switch (codec) { | |
134 case media::kCodecVorbis: | |
135 return PP_AUDIOCODEC_VORBIS; | |
136 case media::kCodecAAC: | |
137 return PP_AUDIOCODEC_AAC; | |
138 default: | |
139 return PP_AUDIOCODEC_UNKNOWN; | |
140 } | |
141 } | |
142 | |
143 PP_VideoCodec MediaVideoCodecToPpVideoCodec(media::VideoCodec codec) { | |
144 switch (codec) { | |
145 case media::kCodecVP8: | |
146 return PP_VIDEOCODEC_VP8; | |
147 case media::kCodecH264: | |
148 return PP_VIDEOCODEC_H264; | |
149 default: | |
150 return PP_VIDEOCODEC_UNKNOWN; | |
151 } | |
152 } | |
153 | |
154 PP_VideoCodecProfile MediaVideoCodecProfileToPpVideoCodecProfile( | |
155 media::VideoCodecProfile profile) { | |
156 switch (profile) { | |
157 case media::VP8PROFILE_MAIN: | |
158 return PP_VIDEOCODECPROFILE_VP8_MAIN; | |
159 case media::H264PROFILE_BASELINE: | |
160 return PP_VIDEOCODECPROFILE_H264_BASELINE; | |
161 case media::H264PROFILE_MAIN: | |
162 return PP_VIDEOCODECPROFILE_H264_MAIN; | |
163 case media::H264PROFILE_EXTENDED: | |
164 return PP_VIDEOCODECPROFILE_H264_EXTENDED; | |
165 case media::H264PROFILE_HIGH: | |
166 return PP_VIDEOCODECPROFILE_H264_HIGH; | |
167 case media::H264PROFILE_HIGH10PROFILE: | |
168 return PP_VIDEOCODECPROFILE_H264_HIGH_10; | |
169 case media::H264PROFILE_HIGH422PROFILE: | |
170 return PP_VIDEOCODECPROFILE_H264_HIGH_422; | |
171 case media::H264PROFILE_HIGH444PREDICTIVEPROFILE: | |
172 return PP_VIDEOCODECPROFILE_H264_HIGH_444_PREDICTIVE; | |
173 default: | |
174 return PP_VIDEOCODECPROFILE_UNKNOWN; | |
175 } | |
176 } | |
177 | |
178 PP_DecryptedFrameFormat MediaVideoFormatToPpDecryptedFrameFormat( | |
179 media::VideoFrame::Format format) { | |
180 switch (format) { | |
181 case media::VideoFrame::YV12: | |
182 return PP_DECRYPTEDFRAMEFORMAT_YV12; | |
183 case media::VideoFrame::I420: | |
184 return PP_DECRYPTEDFRAMEFORMAT_I420; | |
185 default: | |
186 return PP_DECRYPTEDFRAMEFORMAT_UNKNOWN; | |
187 } | |
188 } | |
189 | |
190 media::Decryptor::Status PpDecryptResultToMediaDecryptorStatus( | |
191 PP_DecryptResult result) { | |
192 switch (result) { | |
193 case PP_DECRYPTRESULT_SUCCESS: | |
194 return media::Decryptor::kSuccess; | |
195 case PP_DECRYPTRESULT_DECRYPT_NOKEY: | |
196 return media::Decryptor::kNoKey; | |
197 case PP_DECRYPTRESULT_NEEDMOREDATA: | |
198 return media::Decryptor::kNeedMoreData; | |
199 case PP_DECRYPTRESULT_DECRYPT_ERROR: | |
200 return media::Decryptor::kError; | |
201 case PP_DECRYPTRESULT_DECODE_ERROR: | |
202 return media::Decryptor::kError; | |
203 default: | |
204 NOTREACHED(); | |
205 return media::Decryptor::kError; | |
206 } | |
207 } | |
208 | |
209 PP_DecryptorStreamType MediaDecryptorStreamTypeToPpStreamType( | |
210 media::Decryptor::StreamType stream_type) { | |
211 switch (stream_type) { | |
212 case media::Decryptor::kAudio: | |
213 return PP_DECRYPTORSTREAMTYPE_AUDIO; | |
214 case media::Decryptor::kVideo: | |
215 return PP_DECRYPTORSTREAMTYPE_VIDEO; | |
216 default: | |
217 NOTREACHED(); | |
218 return PP_DECRYPTORSTREAMTYPE_VIDEO; | |
219 } | |
220 } | |
221 | |
222 } // namespace | |
223 | |
224 ContentDecryptorDelegate::ContentDecryptorDelegate( | |
225 PP_Instance pp_instance, | |
226 const PPP_ContentDecryptor_Private* plugin_decryption_interface) | |
227 : pp_instance_(pp_instance), | |
228 plugin_decryption_interface_(plugin_decryption_interface), | |
229 next_decryption_request_id_(1), | |
230 pending_audio_decrypt_request_id_(0), | |
231 pending_video_decrypt_request_id_(0), | |
232 pending_audio_decoder_init_request_id_(0), | |
233 pending_video_decoder_init_request_id_(0), | |
234 pending_audio_decode_request_id_(0), | |
235 pending_video_decode_request_id_(0), | |
236 weak_ptr_factory_(this), | |
237 weak_this_(weak_ptr_factory_.GetWeakPtr()), | |
238 audio_sample_format_(media::kUnknownSampleFormat), | |
239 audio_samples_per_second_(0), | |
240 audio_channel_count_(0), | |
241 audio_bytes_per_frame_(0) { | |
242 } | |
243 | |
244 void ContentDecryptorDelegate::Initialize(const std::string& key_system) { | |
245 // TODO(ddorwin): Add an Initialize method to PPP_ContentDecryptor_Private. | |
246 DCHECK(!key_system.empty()); | |
247 key_system_ = key_system; | |
248 } | |
249 | |
250 void ContentDecryptorDelegate::SetKeyEventCallbacks( | |
251 const media::KeyAddedCB& key_added_cb, | |
252 const media::KeyErrorCB& key_error_cb, | |
253 const media::KeyMessageCB& key_message_cb) { | |
254 key_added_cb_ = key_added_cb; | |
255 key_error_cb_ = key_error_cb; | |
256 key_message_cb_ = key_message_cb; | |
257 } | |
258 | |
259 bool ContentDecryptorDelegate::GenerateKeyRequest(const std::string& type, | |
260 const uint8* init_data, | |
261 int init_data_length) { | |
262 PP_Var init_data_array = | |
263 PpapiGlobals::Get()->GetVarTracker()->MakeArrayBufferPPVar( | |
264 init_data_length, init_data); | |
265 | |
266 plugin_decryption_interface_->GenerateKeyRequest( | |
267 pp_instance_, | |
268 StringVar::StringToPPVar(key_system_), // TODO(ddorwin): Remove. | |
269 StringVar::StringToPPVar(type), | |
270 init_data_array); | |
271 return true; | |
272 } | |
273 | |
274 bool ContentDecryptorDelegate::AddKey(const std::string& session_id, | |
275 const uint8* key, | |
276 int key_length, | |
277 const uint8* init_data, | |
278 int init_data_length) { | |
279 PP_Var key_array = | |
280 PpapiGlobals::Get()->GetVarTracker()->MakeArrayBufferPPVar(key_length, | |
281 key); | |
282 PP_Var init_data_array = | |
283 PpapiGlobals::Get()->GetVarTracker()->MakeArrayBufferPPVar( | |
284 init_data_length, init_data); | |
285 | |
286 plugin_decryption_interface_->AddKey( | |
287 pp_instance_, | |
288 StringVar::StringToPPVar(session_id), | |
289 key_array, | |
290 init_data_array); | |
291 return true; | |
292 } | |
293 | |
294 bool ContentDecryptorDelegate::CancelKeyRequest(const std::string& session_id) { | |
295 plugin_decryption_interface_->CancelKeyRequest( | |
296 pp_instance_, | |
297 StringVar::StringToPPVar(session_id)); | |
298 return true; | |
299 } | |
300 | |
301 // TODO(xhwang): Remove duplication of code in Decrypt(), | |
302 // DecryptAndDecodeAudio() and DecryptAndDecodeVideo(). | |
303 bool ContentDecryptorDelegate::Decrypt( | |
304 media::Decryptor::StreamType stream_type, | |
305 const scoped_refptr<media::DecoderBuffer>& encrypted_buffer, | |
306 const media::Decryptor::DecryptCB& decrypt_cb) { | |
307 DVLOG(3) << "Decrypt() - stream_type: " << stream_type; | |
308 // |{audio|video}_input_resource_| is not being used by the plugin | |
309 // now because there is only one pending audio/video decrypt request at any | |
310 // time. This is enforced by the media pipeline. | |
311 scoped_refptr<PPB_Buffer_Impl> encrypted_resource; | |
312 if (!MakeMediaBufferResource( | |
313 stream_type, encrypted_buffer, &encrypted_resource) || | |
314 !encrypted_resource.get()) { | |
315 return false; | |
316 } | |
317 ScopedPPResource pp_resource(encrypted_resource.get()); | |
318 | |
319 const uint32_t request_id = next_decryption_request_id_++; | |
320 DVLOG(2) << "Decrypt() - request_id " << request_id; | |
321 | |
322 PP_EncryptedBlockInfo block_info = {}; | |
323 DCHECK(encrypted_buffer->decrypt_config()); | |
324 if (!MakeEncryptedBlockInfo(encrypted_buffer, request_id, &block_info)) { | |
325 return false; | |
326 } | |
327 | |
328 // There is only one pending decrypt request at any time per stream. This is | |
329 // enforced by the media pipeline. | |
330 switch (stream_type) { | |
331 case media::Decryptor::kAudio: | |
332 DCHECK_EQ(pending_audio_decrypt_request_id_, 0u); | |
333 DCHECK(pending_audio_decrypt_cb_.is_null()); | |
334 pending_audio_decrypt_request_id_ = request_id; | |
335 pending_audio_decrypt_cb_ = decrypt_cb; | |
336 break; | |
337 case media::Decryptor::kVideo: | |
338 DCHECK_EQ(pending_video_decrypt_request_id_, 0u); | |
339 DCHECK(pending_video_decrypt_cb_.is_null()); | |
340 pending_video_decrypt_request_id_ = request_id; | |
341 pending_video_decrypt_cb_ = decrypt_cb; | |
342 break; | |
343 default: | |
344 NOTREACHED(); | |
345 return false; | |
346 } | |
347 | |
348 SetBufferToFreeInTrackingInfo(&block_info.tracking_info); | |
349 | |
350 plugin_decryption_interface_->Decrypt(pp_instance_, | |
351 pp_resource, | |
352 &block_info); | |
353 return true; | |
354 } | |
355 | |
356 bool ContentDecryptorDelegate::CancelDecrypt( | |
357 media::Decryptor::StreamType stream_type) { | |
358 DVLOG(3) << "CancelDecrypt() - stream_type: " << stream_type; | |
359 | |
360 media::Decryptor::DecryptCB decrypt_cb; | |
361 switch (stream_type) { | |
362 case media::Decryptor::kAudio: | |
363 // Release the shared memory as it can still be in use by the plugin. | |
364 // The next Decrypt() call will need to allocate a new shared memory | |
365 // buffer. | |
366 audio_input_resource_ = NULL; | |
367 pending_audio_decrypt_request_id_ = 0; | |
368 decrypt_cb = base::ResetAndReturn(&pending_audio_decrypt_cb_); | |
369 break; | |
370 case media::Decryptor::kVideo: | |
371 // Release the shared memory as it can still be in use by the plugin. | |
372 // The next Decrypt() call will need to allocate a new shared memory | |
373 // buffer. | |
374 video_input_resource_ = NULL; | |
375 pending_video_decrypt_request_id_ = 0; | |
376 decrypt_cb = base::ResetAndReturn(&pending_video_decrypt_cb_); | |
377 break; | |
378 default: | |
379 NOTREACHED(); | |
380 return false; | |
381 } | |
382 | |
383 if (!decrypt_cb.is_null()) | |
384 decrypt_cb.Run(media::Decryptor::kSuccess, NULL); | |
385 | |
386 return true; | |
387 } | |
388 | |
389 bool ContentDecryptorDelegate::InitializeAudioDecoder( | |
390 const media::AudioDecoderConfig& decoder_config, | |
391 const media::Decryptor::DecoderInitCB& init_cb) { | |
392 PP_AudioDecoderConfig pp_decoder_config; | |
393 pp_decoder_config.codec = | |
394 MediaAudioCodecToPpAudioCodec(decoder_config.codec()); | |
395 pp_decoder_config.channel_count = | |
396 media::ChannelLayoutToChannelCount(decoder_config.channel_layout()); | |
397 pp_decoder_config.bits_per_channel = decoder_config.bits_per_channel(); | |
398 pp_decoder_config.samples_per_second = decoder_config.samples_per_second(); | |
399 pp_decoder_config.request_id = next_decryption_request_id_++; | |
400 | |
401 audio_sample_format_ = decoder_config.sample_format(); | |
402 audio_samples_per_second_ = pp_decoder_config.samples_per_second; | |
403 audio_channel_count_ = pp_decoder_config.channel_count; | |
404 audio_bytes_per_frame_ = decoder_config.bytes_per_frame(); | |
405 | |
406 scoped_refptr<PPB_Buffer_Impl> extra_data_resource; | |
407 if (!MakeBufferResource(pp_instance_, | |
408 decoder_config.extra_data(), | |
409 decoder_config.extra_data_size(), | |
410 &extra_data_resource)) { | |
411 return false; | |
412 } | |
413 ScopedPPResource pp_resource(extra_data_resource.get()); | |
414 | |
415 DCHECK_EQ(pending_audio_decoder_init_request_id_, 0u); | |
416 DCHECK(pending_audio_decoder_init_cb_.is_null()); | |
417 pending_audio_decoder_init_request_id_ = pp_decoder_config.request_id; | |
418 pending_audio_decoder_init_cb_ = init_cb; | |
419 | |
420 plugin_decryption_interface_->InitializeAudioDecoder(pp_instance_, | |
421 &pp_decoder_config, | |
422 pp_resource); | |
423 return true; | |
424 } | |
425 | |
426 bool ContentDecryptorDelegate::InitializeVideoDecoder( | |
427 const media::VideoDecoderConfig& decoder_config, | |
428 const media::Decryptor::DecoderInitCB& init_cb) { | |
429 PP_VideoDecoderConfig pp_decoder_config; | |
430 pp_decoder_config.codec = | |
431 MediaVideoCodecToPpVideoCodec(decoder_config.codec()); | |
432 pp_decoder_config.profile = | |
433 MediaVideoCodecProfileToPpVideoCodecProfile(decoder_config.profile()); | |
434 pp_decoder_config.format = | |
435 MediaVideoFormatToPpDecryptedFrameFormat(decoder_config.format()); | |
436 pp_decoder_config.width = decoder_config.coded_size().width(); | |
437 pp_decoder_config.height = decoder_config.coded_size().height(); | |
438 pp_decoder_config.request_id = next_decryption_request_id_++; | |
439 | |
440 scoped_refptr<PPB_Buffer_Impl> extra_data_resource; | |
441 if (!MakeBufferResource(pp_instance_, | |
442 decoder_config.extra_data(), | |
443 decoder_config.extra_data_size(), | |
444 &extra_data_resource)) { | |
445 return false; | |
446 } | |
447 ScopedPPResource pp_resource(extra_data_resource.get()); | |
448 | |
449 DCHECK_EQ(pending_video_decoder_init_request_id_, 0u); | |
450 DCHECK(pending_video_decoder_init_cb_.is_null()); | |
451 pending_video_decoder_init_request_id_ = pp_decoder_config.request_id; | |
452 pending_video_decoder_init_cb_ = init_cb; | |
453 | |
454 natural_size_ = decoder_config.natural_size(); | |
455 | |
456 plugin_decryption_interface_->InitializeVideoDecoder(pp_instance_, | |
457 &pp_decoder_config, | |
458 pp_resource); | |
459 return true; | |
460 } | |
461 | |
462 bool ContentDecryptorDelegate::DeinitializeDecoder( | |
463 media::Decryptor::StreamType stream_type) { | |
464 CancelDecode(stream_type); | |
465 | |
466 natural_size_ = gfx::Size(); | |
467 | |
468 // TODO(tomfinegan): Add decoder deinitialize request tracking, and get | |
469 // stream type from media stack. | |
470 plugin_decryption_interface_->DeinitializeDecoder( | |
471 pp_instance_, MediaDecryptorStreamTypeToPpStreamType(stream_type), 0); | |
472 return true; | |
473 } | |
474 | |
475 bool ContentDecryptorDelegate::ResetDecoder( | |
476 media::Decryptor::StreamType stream_type) { | |
477 CancelDecode(stream_type); | |
478 | |
479 // TODO(tomfinegan): Add decoder reset request tracking. | |
480 plugin_decryption_interface_->ResetDecoder( | |
481 pp_instance_, MediaDecryptorStreamTypeToPpStreamType(stream_type), 0); | |
482 return true; | |
483 } | |
484 | |
485 bool ContentDecryptorDelegate::DecryptAndDecodeAudio( | |
486 const scoped_refptr<media::DecoderBuffer>& encrypted_buffer, | |
487 const media::Decryptor::AudioDecodeCB& audio_decode_cb) { | |
488 // |audio_input_resource_| is not being used by the plugin now | |
489 // because there is only one pending audio decode request at any time. | |
490 // This is enforced by the media pipeline. | |
491 scoped_refptr<PPB_Buffer_Impl> encrypted_resource; | |
492 if (!MakeMediaBufferResource(media::Decryptor::kAudio, | |
493 encrypted_buffer, | |
494 &encrypted_resource)) { | |
495 return false; | |
496 } | |
497 | |
498 // The resource should not be NULL for non-EOS buffer. | |
499 if (!encrypted_buffer->end_of_stream() && !encrypted_resource.get()) | |
500 return false; | |
501 | |
502 const uint32_t request_id = next_decryption_request_id_++; | |
503 DVLOG(2) << "DecryptAndDecodeAudio() - request_id " << request_id; | |
504 | |
505 PP_EncryptedBlockInfo block_info = {}; | |
506 if (!MakeEncryptedBlockInfo(encrypted_buffer, request_id, &block_info)) { | |
507 return false; | |
508 } | |
509 | |
510 SetBufferToFreeInTrackingInfo(&block_info.tracking_info); | |
511 | |
512 // There is only one pending audio decode request at any time. This is | |
513 // enforced by the media pipeline. If this DCHECK is violated, our buffer | |
514 // reuse policy is not valid, and we may have race problems for the shared | |
515 // buffer. | |
516 DCHECK_EQ(pending_audio_decode_request_id_, 0u); | |
517 DCHECK(pending_audio_decode_cb_.is_null()); | |
518 pending_audio_decode_request_id_ = request_id; | |
519 pending_audio_decode_cb_ = audio_decode_cb; | |
520 | |
521 ScopedPPResource pp_resource(encrypted_resource.get()); | |
522 plugin_decryption_interface_->DecryptAndDecode(pp_instance_, | |
523 PP_DECRYPTORSTREAMTYPE_AUDIO, | |
524 pp_resource, | |
525 &block_info); | |
526 return true; | |
527 } | |
528 | |
529 bool ContentDecryptorDelegate::DecryptAndDecodeVideo( | |
530 const scoped_refptr<media::DecoderBuffer>& encrypted_buffer, | |
531 const media::Decryptor::VideoDecodeCB& video_decode_cb) { | |
532 // |video_input_resource_| is not being used by the plugin now | |
533 // because there is only one pending video decode request at any time. | |
534 // This is enforced by the media pipeline. | |
535 scoped_refptr<PPB_Buffer_Impl> encrypted_resource; | |
536 if (!MakeMediaBufferResource(media::Decryptor::kVideo, | |
537 encrypted_buffer, | |
538 &encrypted_resource)) { | |
539 return false; | |
540 } | |
541 | |
542 // The resource should not be 0 for non-EOS buffer. | |
543 if (!encrypted_buffer->end_of_stream() && !encrypted_resource.get()) | |
544 return false; | |
545 | |
546 const uint32_t request_id = next_decryption_request_id_++; | |
547 DVLOG(2) << "DecryptAndDecodeVideo() - request_id " << request_id; | |
548 TRACE_EVENT_ASYNC_BEGIN0( | |
549 "eme", "ContentDecryptorDelegate::DecryptAndDecodeVideo", request_id); | |
550 | |
551 PP_EncryptedBlockInfo block_info = {}; | |
552 if (!MakeEncryptedBlockInfo(encrypted_buffer, request_id, &block_info)) { | |
553 return false; | |
554 } | |
555 | |
556 SetBufferToFreeInTrackingInfo(&block_info.tracking_info); | |
557 | |
558 // Only one pending video decode request at any time. This is enforced by the | |
559 // media pipeline. If this DCHECK is violated, our buffer | |
560 // reuse policy is not valid, and we may have race problems for the shared | |
561 // buffer. | |
562 DCHECK_EQ(pending_video_decode_request_id_, 0u); | |
563 DCHECK(pending_video_decode_cb_.is_null()); | |
564 pending_video_decode_request_id_ = request_id; | |
565 pending_video_decode_cb_ = video_decode_cb; | |
566 | |
567 // TODO(tomfinegan): Need to get stream type from media stack. | |
568 ScopedPPResource pp_resource(encrypted_resource.get()); | |
569 plugin_decryption_interface_->DecryptAndDecode(pp_instance_, | |
570 PP_DECRYPTORSTREAMTYPE_VIDEO, | |
571 pp_resource, | |
572 &block_info); | |
573 return true; | |
574 } | |
575 | |
576 void ContentDecryptorDelegate::NeedKey(PP_Var key_system_var, | |
577 PP_Var session_id_var, | |
578 PP_Var init_data_var) { | |
579 // TODO(ddorwin): Remove from PPB_ContentDecryptor_Private. | |
580 NOTREACHED(); | |
581 } | |
582 | |
583 void ContentDecryptorDelegate::KeyAdded(PP_Var key_system_var, | |
584 PP_Var session_id_var) { | |
585 if (key_added_cb_.is_null()) | |
586 return; | |
587 | |
588 StringVar* session_id_string = StringVar::FromPPVar(session_id_var); | |
589 if (!session_id_string) { | |
590 key_error_cb_.Run(std::string(), media::MediaKeys::kUnknownError, 0); | |
591 return; | |
592 } | |
593 | |
594 key_added_cb_.Run(session_id_string->value()); | |
595 } | |
596 | |
597 void ContentDecryptorDelegate::KeyMessage(PP_Var key_system_var, | |
598 PP_Var session_id_var, | |
599 PP_Var message_var, | |
600 PP_Var default_url_var) { | |
601 if (key_message_cb_.is_null()) | |
602 return; | |
603 | |
604 StringVar* session_id_string = StringVar::FromPPVar(session_id_var); | |
605 | |
606 ArrayBufferVar* message_array_buffer = | |
607 ArrayBufferVar::FromPPVar(message_var); | |
608 | |
609 std::vector<uint8> message; | |
610 if (message_array_buffer) { | |
611 const uint8* data = static_cast<const uint8*>(message_array_buffer->Map()); | |
612 message.assign(data, data + message_array_buffer->ByteLength()); | |
613 } | |
614 | |
615 StringVar* default_url_string = StringVar::FromPPVar(default_url_var); | |
616 | |
617 if (!session_id_string || !default_url_string) { | |
618 key_error_cb_.Run(std::string(), media::MediaKeys::kUnknownError, 0); | |
619 return; | |
620 } | |
621 | |
622 key_message_cb_.Run(session_id_string->value(), | |
623 message, | |
624 default_url_string->value()); | |
625 } | |
626 | |
627 void ContentDecryptorDelegate::KeyError(PP_Var key_system_var, | |
628 PP_Var session_id_var, | |
629 int32_t media_error, | |
630 int32_t system_code) { | |
631 if (key_error_cb_.is_null()) | |
632 return; | |
633 | |
634 StringVar* session_id_string = StringVar::FromPPVar(session_id_var); | |
635 if (!session_id_string) { | |
636 key_error_cb_.Run(std::string(), media::MediaKeys::kUnknownError, 0); | |
637 return; | |
638 } | |
639 | |
640 key_error_cb_.Run(session_id_string->value(), | |
641 static_cast<media::MediaKeys::KeyError>(media_error), | |
642 system_code); | |
643 } | |
644 | |
645 void ContentDecryptorDelegate::DecoderInitializeDone( | |
646 PP_DecryptorStreamType decoder_type, | |
647 uint32_t request_id, | |
648 PP_Bool success) { | |
649 if (decoder_type == PP_DECRYPTORSTREAMTYPE_AUDIO) { | |
650 // If the request ID is not valid or does not match what's saved, do | |
651 // nothing. | |
652 if (request_id == 0 || | |
653 request_id != pending_audio_decoder_init_request_id_) | |
654 return; | |
655 | |
656 DCHECK(!pending_audio_decoder_init_cb_.is_null()); | |
657 pending_audio_decoder_init_request_id_ = 0; | |
658 base::ResetAndReturn( | |
659 &pending_audio_decoder_init_cb_).Run(PP_ToBool(success)); | |
660 } else { | |
661 if (request_id == 0 || | |
662 request_id != pending_video_decoder_init_request_id_) | |
663 return; | |
664 | |
665 if (!success) | |
666 natural_size_ = gfx::Size(); | |
667 | |
668 DCHECK(!pending_video_decoder_init_cb_.is_null()); | |
669 pending_video_decoder_init_request_id_ = 0; | |
670 base::ResetAndReturn( | |
671 &pending_video_decoder_init_cb_).Run(PP_ToBool(success)); | |
672 } | |
673 } | |
674 | |
675 void ContentDecryptorDelegate::DecoderDeinitializeDone( | |
676 PP_DecryptorStreamType decoder_type, | |
677 uint32_t request_id) { | |
678 // TODO(tomfinegan): Add decoder stop completion handling. | |
679 } | |
680 | |
681 void ContentDecryptorDelegate::DecoderResetDone( | |
682 PP_DecryptorStreamType decoder_type, | |
683 uint32_t request_id) { | |
684 // TODO(tomfinegan): Add decoder reset completion handling. | |
685 } | |
686 | |
687 void ContentDecryptorDelegate::DeliverBlock( | |
688 PP_Resource decrypted_block, | |
689 const PP_DecryptedBlockInfo* block_info) { | |
690 DCHECK(block_info); | |
691 | |
692 FreeBuffer(block_info->tracking_info.buffer_id); | |
693 | |
694 const uint32_t request_id = block_info->tracking_info.request_id; | |
695 DVLOG(2) << "DeliverBlock() - request_id: " << request_id; | |
696 | |
697 // If the request ID is not valid or does not match what's saved, do nothing. | |
698 if (request_id == 0) { | |
699 DVLOG(1) << "DeliverBlock() - invalid request_id " << request_id; | |
700 return; | |
701 } | |
702 | |
703 media::Decryptor::DecryptCB decrypt_cb; | |
704 if (request_id == pending_audio_decrypt_request_id_) { | |
705 DCHECK(!pending_audio_decrypt_cb_.is_null()); | |
706 pending_audio_decrypt_request_id_ = 0; | |
707 decrypt_cb = base::ResetAndReturn(&pending_audio_decrypt_cb_); | |
708 } else if (request_id == pending_video_decrypt_request_id_) { | |
709 DCHECK(!pending_video_decrypt_cb_.is_null()); | |
710 pending_video_decrypt_request_id_ = 0; | |
711 decrypt_cb = base::ResetAndReturn(&pending_video_decrypt_cb_); | |
712 } else { | |
713 DVLOG(1) << "DeliverBlock() - request_id " << request_id << " not found"; | |
714 return; | |
715 } | |
716 | |
717 media::Decryptor::Status status = | |
718 PpDecryptResultToMediaDecryptorStatus(block_info->result); | |
719 if (status != media::Decryptor::kSuccess) { | |
720 decrypt_cb.Run(status, NULL); | |
721 return; | |
722 } | |
723 | |
724 EnterResourceNoLock<PPB_Buffer_API> enter(decrypted_block, true); | |
725 if (!enter.succeeded()) { | |
726 decrypt_cb.Run(media::Decryptor::kError, NULL); | |
727 return; | |
728 } | |
729 BufferAutoMapper mapper(enter.object()); | |
730 if (!mapper.data() || !mapper.size() || | |
731 mapper.size() < block_info->data_size) { | |
732 decrypt_cb.Run(media::Decryptor::kError, NULL); | |
733 return; | |
734 } | |
735 | |
736 // TODO(tomfinegan): Find a way to take ownership of the shared memory | |
737 // managed by the PPB_Buffer_Dev, and avoid the extra copy. | |
738 scoped_refptr<media::DecoderBuffer> decrypted_buffer( | |
739 media::DecoderBuffer::CopyFrom( | |
740 static_cast<uint8*>(mapper.data()), block_info->data_size)); | |
741 decrypted_buffer->set_timestamp(base::TimeDelta::FromMicroseconds( | |
742 block_info->tracking_info.timestamp)); | |
743 decrypt_cb.Run(media::Decryptor::kSuccess, decrypted_buffer); | |
744 } | |
745 | |
746 // Use a non-class-member function here so that if for some reason | |
747 // ContentDecryptorDelegate is destroyed before VideoFrame calls this callback, | |
748 // we can still get the shared memory unmapped. | |
749 static void BufferNoLongerNeeded( | |
750 const scoped_refptr<PPB_Buffer_Impl>& ppb_buffer, | |
751 base::Closure buffer_no_longer_needed_cb) { | |
752 ppb_buffer->Unmap(); | |
753 buffer_no_longer_needed_cb.Run(); | |
754 } | |
755 | |
756 // Enters |resource|, maps shared memory and returns pointer of mapped data. | |
757 // Returns NULL if any error occurs. | |
758 static uint8* GetMappedBuffer(PP_Resource resource, | |
759 scoped_refptr<PPB_Buffer_Impl>* ppb_buffer) { | |
760 EnterResourceNoLock<PPB_Buffer_API> enter(resource, true); | |
761 if (!enter.succeeded()) | |
762 return NULL; | |
763 | |
764 uint8* mapped_data = static_cast<uint8*>(enter.object()->Map()); | |
765 if (!enter.object()->IsMapped() || !mapped_data) | |
766 return NULL; | |
767 | |
768 uint32_t mapped_size = 0; | |
769 if (!enter.object()->Describe(&mapped_size) || !mapped_size) { | |
770 enter.object()->Unmap(); | |
771 return NULL; | |
772 } | |
773 | |
774 *ppb_buffer = static_cast<PPB_Buffer_Impl*>(enter.object()); | |
775 | |
776 return mapped_data; | |
777 } | |
778 | |
779 void ContentDecryptorDelegate::DeliverFrame( | |
780 PP_Resource decrypted_frame, | |
781 const PP_DecryptedFrameInfo* frame_info) { | |
782 DCHECK(frame_info); | |
783 | |
784 const uint32_t request_id = frame_info->tracking_info.request_id; | |
785 DVLOG(2) << "DeliverFrame() - request_id: " << request_id; | |
786 | |
787 // If the request ID is not valid or does not match what's saved, do nothing. | |
788 if (request_id == 0 || request_id != pending_video_decode_request_id_) { | |
789 DVLOG(1) << "DeliverFrame() - request_id " << request_id << " not found"; | |
790 FreeBuffer(frame_info->tracking_info.buffer_id); | |
791 return; | |
792 } | |
793 | |
794 TRACE_EVENT_ASYNC_END0( | |
795 "eme", "ContentDecryptorDelegate::DecryptAndDecodeVideo", request_id); | |
796 | |
797 DCHECK(!pending_video_decode_cb_.is_null()); | |
798 pending_video_decode_request_id_ = 0; | |
799 media::Decryptor::VideoDecodeCB video_decode_cb = | |
800 base::ResetAndReturn(&pending_video_decode_cb_); | |
801 | |
802 media::Decryptor::Status status = | |
803 PpDecryptResultToMediaDecryptorStatus(frame_info->result); | |
804 if (status != media::Decryptor::kSuccess) { | |
805 DCHECK(!frame_info->tracking_info.buffer_id); | |
806 video_decode_cb.Run(status, NULL); | |
807 return; | |
808 } | |
809 | |
810 scoped_refptr<PPB_Buffer_Impl> ppb_buffer; | |
811 uint8* frame_data = GetMappedBuffer(decrypted_frame, &ppb_buffer); | |
812 if (!frame_data) { | |
813 FreeBuffer(frame_info->tracking_info.buffer_id); | |
814 video_decode_cb.Run(media::Decryptor::kError, NULL); | |
815 return; | |
816 } | |
817 | |
818 gfx::Size frame_size(frame_info->width, frame_info->height); | |
819 DCHECK_EQ(frame_info->format, PP_DECRYPTEDFRAMEFORMAT_YV12); | |
820 | |
821 scoped_refptr<media::VideoFrame> decoded_frame = | |
822 media::VideoFrame::WrapExternalYuvData( | |
823 media::VideoFrame::YV12, | |
824 frame_size, gfx::Rect(frame_size), natural_size_, | |
825 frame_info->strides[PP_DECRYPTEDFRAMEPLANES_Y], | |
826 frame_info->strides[PP_DECRYPTEDFRAMEPLANES_U], | |
827 frame_info->strides[PP_DECRYPTEDFRAMEPLANES_V], | |
828 frame_data + frame_info->plane_offsets[PP_DECRYPTEDFRAMEPLANES_Y], | |
829 frame_data + frame_info->plane_offsets[PP_DECRYPTEDFRAMEPLANES_U], | |
830 frame_data + frame_info->plane_offsets[PP_DECRYPTEDFRAMEPLANES_V], | |
831 base::TimeDelta::FromMicroseconds( | |
832 frame_info->tracking_info.timestamp), | |
833 ppb_buffer->shared_memory()->handle(), | |
834 media::BindToLoop( | |
835 base::MessageLoopProxy::current(), | |
836 base::Bind(&BufferNoLongerNeeded, ppb_buffer, | |
837 base::Bind(&ContentDecryptorDelegate::FreeBuffer, | |
838 weak_this_, | |
839 frame_info->tracking_info.buffer_id)))); | |
840 | |
841 video_decode_cb.Run(media::Decryptor::kSuccess, decoded_frame); | |
842 } | |
843 | |
844 void ContentDecryptorDelegate::DeliverSamples( | |
845 PP_Resource audio_frames, | |
846 const PP_DecryptedBlockInfo* block_info) { | |
847 DCHECK(block_info); | |
848 | |
849 FreeBuffer(block_info->tracking_info.buffer_id); | |
850 | |
851 const uint32_t request_id = block_info->tracking_info.request_id; | |
852 DVLOG(2) << "DeliverSamples() - request_id: " << request_id; | |
853 | |
854 // If the request ID is not valid or does not match what's saved, do nothing. | |
855 if (request_id == 0 || request_id != pending_audio_decode_request_id_) { | |
856 DVLOG(1) << "DeliverSamples() - request_id " << request_id << " not found"; | |
857 return; | |
858 } | |
859 | |
860 DCHECK(!pending_audio_decode_cb_.is_null()); | |
861 pending_audio_decode_request_id_ = 0; | |
862 media::Decryptor::AudioDecodeCB audio_decode_cb = | |
863 base::ResetAndReturn(&pending_audio_decode_cb_); | |
864 | |
865 const media::Decryptor::AudioBuffers empty_frames; | |
866 | |
867 media::Decryptor::Status status = | |
868 PpDecryptResultToMediaDecryptorStatus(block_info->result); | |
869 if (status != media::Decryptor::kSuccess) { | |
870 audio_decode_cb.Run(status, empty_frames); | |
871 return; | |
872 } | |
873 | |
874 media::Decryptor::AudioBuffers audio_frame_list; | |
875 if (!DeserializeAudioFrames(audio_frames, | |
876 block_info->data_size, | |
877 &audio_frame_list)) { | |
878 NOTREACHED() << "CDM did not serialize the buffer correctly."; | |
879 audio_decode_cb.Run(media::Decryptor::kError, empty_frames); | |
880 return; | |
881 } | |
882 | |
883 audio_decode_cb.Run(media::Decryptor::kSuccess, audio_frame_list); | |
884 } | |
885 | |
886 // TODO(xhwang): Try to remove duplicate logic here and in CancelDecrypt(). | |
887 void ContentDecryptorDelegate::CancelDecode( | |
888 media::Decryptor::StreamType stream_type) { | |
889 switch (stream_type) { | |
890 case media::Decryptor::kAudio: | |
891 // Release the shared memory as it can still be in use by the plugin. | |
892 // The next DecryptAndDecode() call will need to allocate a new shared | |
893 // memory buffer. | |
894 audio_input_resource_ = NULL; | |
895 pending_audio_decode_request_id_ = 0; | |
896 if (!pending_audio_decode_cb_.is_null()) | |
897 base::ResetAndReturn(&pending_audio_decode_cb_).Run( | |
898 media::Decryptor::kSuccess, media::Decryptor::AudioBuffers()); | |
899 break; | |
900 case media::Decryptor::kVideo: | |
901 // Release the shared memory as it can still be in use by the plugin. | |
902 // The next DecryptAndDecode() call will need to allocate a new shared | |
903 // memory buffer. | |
904 video_input_resource_ = NULL; | |
905 pending_video_decode_request_id_ = 0; | |
906 if (!pending_video_decode_cb_.is_null()) | |
907 base::ResetAndReturn(&pending_video_decode_cb_).Run( | |
908 media::Decryptor::kSuccess, NULL); | |
909 break; | |
910 default: | |
911 NOTREACHED(); | |
912 } | |
913 } | |
914 | |
915 bool ContentDecryptorDelegate::MakeMediaBufferResource( | |
916 media::Decryptor::StreamType stream_type, | |
917 const scoped_refptr<media::DecoderBuffer>& encrypted_buffer, | |
918 scoped_refptr<PPB_Buffer_Impl>* resource) { | |
919 TRACE_EVENT0("eme", "ContentDecryptorDelegate::MakeMediaBufferResource"); | |
920 | |
921 // End of stream buffers are represented as null resources. | |
922 if (encrypted_buffer->end_of_stream()) { | |
923 *resource = NULL; | |
924 return true; | |
925 } | |
926 | |
927 DCHECK(stream_type == media::Decryptor::kAudio || | |
928 stream_type == media::Decryptor::kVideo); | |
929 scoped_refptr<PPB_Buffer_Impl>& media_resource = | |
930 (stream_type == media::Decryptor::kAudio) ? audio_input_resource_ : | |
931 video_input_resource_; | |
932 | |
933 const size_t data_size = static_cast<size_t>(encrypted_buffer->data_size()); | |
934 if (!media_resource.get() || media_resource->size() < data_size) { | |
935 // Either the buffer hasn't been created yet, or we have one that isn't big | |
936 // enough to fit |size| bytes. | |
937 | |
938 // Media resource size starts from |kMinimumMediaBufferSize| and grows | |
939 // exponentially to avoid frequent re-allocation of PPB_Buffer_Impl, | |
940 // which is usually expensive. Since input media buffers are compressed, | |
941 // they are usually small (compared to outputs). The over-allocated memory | |
942 // should be negligible. | |
943 const uint32_t kMinimumMediaBufferSize = 1024; | |
944 uint32_t media_resource_size = | |
945 media_resource.get() ? media_resource->size() : kMinimumMediaBufferSize; | |
946 while (media_resource_size < data_size) | |
947 media_resource_size *= 2; | |
948 | |
949 DVLOG(2) << "Size of media buffer for " | |
950 << ((stream_type == media::Decryptor::kAudio) ? "audio" : "video") | |
951 << " stream bumped to " << media_resource_size | |
952 << " bytes to fit input."; | |
953 media_resource = PPB_Buffer_Impl::CreateResource(pp_instance_, | |
954 media_resource_size); | |
955 if (!media_resource.get()) | |
956 return false; | |
957 } | |
958 | |
959 BufferAutoMapper mapper(media_resource.get()); | |
960 if (!mapper.data() || mapper.size() < data_size) { | |
961 media_resource = NULL; | |
962 return false; | |
963 } | |
964 memcpy(mapper.data(), encrypted_buffer->data(), data_size); | |
965 | |
966 *resource = media_resource; | |
967 return true; | |
968 } | |
969 | |
970 void ContentDecryptorDelegate::FreeBuffer(uint32_t buffer_id) { | |
971 if (buffer_id) | |
972 free_buffers_.push(buffer_id); | |
973 } | |
974 | |
975 void ContentDecryptorDelegate::SetBufferToFreeInTrackingInfo( | |
976 PP_DecryptTrackingInfo* tracking_info) { | |
977 DCHECK_EQ(tracking_info->buffer_id, 0u); | |
978 | |
979 if (free_buffers_.empty()) | |
980 return; | |
981 | |
982 tracking_info->buffer_id = free_buffers_.front(); | |
983 free_buffers_.pop(); | |
984 } | |
985 | |
986 bool ContentDecryptorDelegate::DeserializeAudioFrames( | |
987 PP_Resource audio_frames, | |
988 size_t data_size, | |
989 media::Decryptor::AudioBuffers* frames) { | |
990 DCHECK(frames); | |
991 EnterResourceNoLock<PPB_Buffer_API> enter(audio_frames, true); | |
992 if (!enter.succeeded()) | |
993 return false; | |
994 | |
995 BufferAutoMapper mapper(enter.object()); | |
996 if (!mapper.data() || !mapper.size() || | |
997 mapper.size() < static_cast<uint32_t>(data_size)) | |
998 return false; | |
999 | |
1000 // TODO(jrummell): Pass ownership of data() directly to AudioBuffer to avoid | |
1001 // the copy. Since it is possible to get multiple buffers, it would need to be | |
1002 // sliced and ref counted appropriately. http://crbug.com/255576. | |
1003 const uint8* cur = static_cast<uint8*>(mapper.data()); | |
1004 size_t bytes_left = data_size; | |
1005 | |
1006 do { | |
1007 int64 timestamp = 0; | |
1008 int64 frame_size = -1; | |
1009 const size_t kHeaderSize = sizeof(timestamp) + sizeof(frame_size); | |
1010 | |
1011 if (bytes_left < kHeaderSize) | |
1012 return false; | |
1013 | |
1014 memcpy(×tamp, cur, sizeof(timestamp)); | |
1015 cur += sizeof(timestamp); | |
1016 bytes_left -= sizeof(timestamp); | |
1017 | |
1018 memcpy(&frame_size, cur, sizeof(frame_size)); | |
1019 cur += sizeof(frame_size); | |
1020 bytes_left -= sizeof(frame_size); | |
1021 | |
1022 // We should *not* have empty frames in the list. | |
1023 if (frame_size <= 0 || | |
1024 bytes_left < base::checked_numeric_cast<size_t>(frame_size)) { | |
1025 return false; | |
1026 } | |
1027 | |
1028 const uint8* data[] = {cur}; | |
1029 int frame_count = frame_size / audio_bytes_per_frame_; | |
1030 scoped_refptr<media::AudioBuffer> frame = media::AudioBuffer::CopyFrom( | |
1031 audio_sample_format_, | |
1032 audio_channel_count_, | |
1033 frame_count, | |
1034 data, | |
1035 base::TimeDelta::FromMicroseconds(timestamp), | |
1036 base::TimeDelta::FromMicroseconds(audio_samples_per_second_ / | |
1037 frame_count)); | |
1038 frames->push_back(frame); | |
1039 | |
1040 cur += frame_size; | |
1041 bytes_left -= frame_size; | |
1042 } while (bytes_left > 0); | |
1043 | |
1044 return true; | |
1045 } | |
1046 | |
1047 } // namespace ppapi | |
1048 } // namespace webkit | |
OLD | NEW |