OLD | NEW |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 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 "chrome/renderer/extensions/cast_streaming_native_handler.h" | 5 #include "chrome/renderer/extensions/cast_streaming_native_handler.h" |
6 | 6 |
7 #include <functional> | 7 #include <functional> |
8 #include <iterator> | 8 #include <iterator> |
9 | 9 |
10 #include "base/logging.h" | 10 #include "base/logging.h" |
11 #include "base/message_loop/message_loop.h" | 11 #include "base/message_loop/message_loop.h" |
12 #include "base/strings/string_number_conversions.h" | 12 #include "base/strings/string_number_conversions.h" |
| 13 #include "chrome/common/extensions/api/cast_streaming_receiver_session.h" |
13 #include "chrome/common/extensions/api/cast_streaming_rtp_stream.h" | 14 #include "chrome/common/extensions/api/cast_streaming_rtp_stream.h" |
14 #include "chrome/common/extensions/api/cast_streaming_udp_transport.h" | 15 #include "chrome/common/extensions/api/cast_streaming_udp_transport.h" |
| 16 #include "chrome/renderer/media/cast_receiver_session.h" |
15 #include "chrome/renderer/media/cast_rtp_stream.h" | 17 #include "chrome/renderer/media/cast_rtp_stream.h" |
16 #include "chrome/renderer/media/cast_session.h" | 18 #include "chrome/renderer/media/cast_session.h" |
17 #include "chrome/renderer/media/cast_udp_transport.h" | 19 #include "chrome/renderer/media/cast_udp_transport.h" |
18 #include "content/public/child/v8_value_converter.h" | 20 #include "content/public/child/v8_value_converter.h" |
| 21 #include "content/public/renderer/media_stream_api.h" |
19 #include "extensions/renderer/script_context.h" | 22 #include "extensions/renderer/script_context.h" |
| 23 #include "media/audio/audio_parameters.h" |
20 #include "net/base/host_port_pair.h" | 24 #include "net/base/host_port_pair.h" |
| 25 #include "third_party/WebKit/public/platform/WebMediaStream.h" |
21 #include "third_party/WebKit/public/platform/WebMediaStreamTrack.h" | 26 #include "third_party/WebKit/public/platform/WebMediaStreamTrack.h" |
| 27 #include "third_party/WebKit/public/platform/WebURL.h" |
22 #include "third_party/WebKit/public/web/WebDOMMediaStreamTrack.h" | 28 #include "third_party/WebKit/public/web/WebDOMMediaStreamTrack.h" |
| 29 #include "third_party/WebKit/public/web/WebMediaStreamRegistry.h" |
| 30 #include "url/gurl.h" |
23 | 31 |
24 using content::V8ValueConverter; | 32 using content::V8ValueConverter; |
25 | 33 |
26 // Extension types. | 34 // Extension types. |
| 35 using extensions::api::cast_streaming_receiver_session::RtpReceiverParams; |
27 using extensions::api::cast_streaming_rtp_stream::CodecSpecificParams; | 36 using extensions::api::cast_streaming_rtp_stream::CodecSpecificParams; |
28 using extensions::api::cast_streaming_rtp_stream::RtpParams; | 37 using extensions::api::cast_streaming_rtp_stream::RtpParams; |
29 using extensions::api::cast_streaming_rtp_stream::RtpPayloadParams; | 38 using extensions::api::cast_streaming_rtp_stream::RtpPayloadParams; |
30 using extensions::api::cast_streaming_udp_transport::IPEndPoint; | 39 using extensions::api::cast_streaming_udp_transport::IPEndPoint; |
31 | 40 |
32 namespace extensions { | 41 namespace extensions { |
33 | 42 |
34 namespace { | 43 namespace { |
| 44 const char kInvalidAesIvMask[] = "Invalid value for AES IV mask"; |
| 45 const char kInvalidAesKey[] = "Invalid value for AES key"; |
| 46 const char kInvalidAudioParams[] = "Invalid audio params"; |
| 47 const char kInvalidDestination[] = "Invalid destination"; |
| 48 const char kInvalidFPS[] = "Invalid FPS"; |
| 49 const char kInvalidMediaStreamURL[] = "Invalid MediaStream URL"; |
| 50 const char kInvalidRtpParams[] = "Invalid value for RTP params"; |
| 51 const char kInvalidLatency[] = "Invalid value for max_latency. (0-1000)"; |
| 52 const char kInvalidRtpTimebase[] = "Invalid rtp_timebase. (1000-1000000)"; |
| 53 const char kInvalidStreamArgs[] = "Invalid stream arguments"; |
35 const char kRtpStreamNotFound[] = "The RTP stream cannot be found"; | 54 const char kRtpStreamNotFound[] = "The RTP stream cannot be found"; |
36 const char kUdpTransportNotFound[] = "The UDP transport cannot be found"; | 55 const char kUdpTransportNotFound[] = "The UDP transport cannot be found"; |
37 const char kInvalidDestination[] = "Invalid destination"; | |
38 const char kInvalidRtpParams[] = "Invalid value for RTP params"; | |
39 const char kInvalidAesKey[] = "Invalid value for AES key"; | |
40 const char kInvalidAesIvMask[] = "Invalid value for AES IV mask"; | |
41 const char kInvalidStreamArgs[] = "Invalid stream arguments"; | |
42 const char kUnableToConvertArgs[] = "Unable to convert arguments"; | 56 const char kUnableToConvertArgs[] = "Unable to convert arguments"; |
43 const char kUnableToConvertParams[] = "Unable to convert params"; | 57 const char kUnableToConvertParams[] = "Unable to convert params"; |
44 | 58 |
45 // These helper methods are used to convert between Extension API | 59 // These helper methods are used to convert between Extension API |
46 // types and Cast types. | 60 // types and Cast types. |
47 void ToCastCodecSpecificParams(const CodecSpecificParams& ext_params, | 61 void ToCastCodecSpecificParams(const CodecSpecificParams& ext_params, |
48 CastCodecSpecificParams* cast_params) { | 62 CastCodecSpecificParams* cast_params) { |
49 cast_params->key = ext_params.key; | 63 cast_params->key = ext_params.key; |
50 cast_params->value = ext_params.value; | 64 cast_params->value = ext_params.value; |
51 } | 65 } |
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
192 base::Unretained(this))); | 206 base::Unretained(this))); |
193 RouteFunction("ToggleLogging", | 207 RouteFunction("ToggleLogging", |
194 base::Bind(&CastStreamingNativeHandler::ToggleLogging, | 208 base::Bind(&CastStreamingNativeHandler::ToggleLogging, |
195 base::Unretained(this))); | 209 base::Unretained(this))); |
196 RouteFunction("GetRawEvents", | 210 RouteFunction("GetRawEvents", |
197 base::Bind(&CastStreamingNativeHandler::GetRawEvents, | 211 base::Bind(&CastStreamingNativeHandler::GetRawEvents, |
198 base::Unretained(this))); | 212 base::Unretained(this))); |
199 RouteFunction("GetStats", | 213 RouteFunction("GetStats", |
200 base::Bind(&CastStreamingNativeHandler::GetStats, | 214 base::Bind(&CastStreamingNativeHandler::GetStats, |
201 base::Unretained(this))); | 215 base::Unretained(this))); |
| 216 RouteFunction("StartCastRtpReceiver", |
| 217 base::Bind(&CastStreamingNativeHandler::StartCastRtpReceiver, |
| 218 base::Unretained(this))); |
202 } | 219 } |
203 | 220 |
204 CastStreamingNativeHandler::~CastStreamingNativeHandler() { | 221 CastStreamingNativeHandler::~CastStreamingNativeHandler() { |
205 } | 222 } |
206 | 223 |
207 void CastStreamingNativeHandler::CreateCastSession( | 224 void CastStreamingNativeHandler::CreateCastSession( |
208 const v8::FunctionCallbackInfo<v8::Value>& args) { | 225 const v8::FunctionCallbackInfo<v8::Value>& args) { |
209 CHECK_EQ(3, args.Length()); | 226 CHECK_EQ(3, args.Length()); |
210 CHECK(args[2]->IsFunction()); | 227 CHECK(args[2]->IsFunction()); |
211 | 228 |
(...skipping 221 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
433 const v8::FunctionCallbackInfo<v8::Value>& args) { | 450 const v8::FunctionCallbackInfo<v8::Value>& args) { |
434 CHECK_EQ(2, args.Length()); | 451 CHECK_EQ(2, args.Length()); |
435 CHECK(args[0]->IsInt32()); | 452 CHECK(args[0]->IsInt32()); |
436 CHECK(args[1]->IsObject()); | 453 CHECK(args[1]->IsObject()); |
437 | 454 |
438 const int transport_id = args[0]->ToInt32(args.GetIsolate())->Value(); | 455 const int transport_id = args[0]->ToInt32(args.GetIsolate())->Value(); |
439 CastUdpTransport* transport = GetUdpTransportOrThrow(transport_id); | 456 CastUdpTransport* transport = GetUdpTransportOrThrow(transport_id); |
440 if (!transport) | 457 if (!transport) |
441 return; | 458 return; |
442 | 459 |
443 scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); | 460 net::IPEndPoint dest; |
444 scoped_ptr<base::Value> destination_value( | 461 if (!IPEndPointFromArg(args.GetIsolate(), |
445 converter->FromV8Value(args[1], context()->v8_context())); | 462 args[1], |
446 if (!destination_value) { | 463 &dest)) { |
447 args.GetIsolate()->ThrowException(v8::Exception::TypeError( | |
448 v8::String::NewFromUtf8(args.GetIsolate(), kUnableToConvertArgs))); | |
449 return; | 464 return; |
450 } | 465 } |
451 scoped_ptr<IPEndPoint> destination = | 466 transport->SetDestination( |
452 IPEndPoint::FromValue(*destination_value); | 467 dest, |
453 if (!destination) { | 468 base::Bind(&CastStreamingNativeHandler::CallErrorCallback, |
454 args.GetIsolate()->ThrowException(v8::Exception::TypeError( | 469 weak_factory_.GetWeakPtr(), |
455 v8::String::NewFromUtf8(args.GetIsolate(), kInvalidDestination))); | 470 transport_id)); |
456 return; | |
457 } | |
458 net::IPAddressNumber ip; | |
459 if (!net::ParseIPLiteralToNumber(destination->address, &ip)) { | |
460 args.GetIsolate()->ThrowException(v8::Exception::TypeError( | |
461 v8::String::NewFromUtf8(args.GetIsolate(), kInvalidDestination))); | |
462 return; | |
463 } | |
464 transport->SetDestination(net::IPEndPoint(ip, destination->port)); | |
465 } | 471 } |
466 | 472 |
467 void CastStreamingNativeHandler::SetOptionsCastUdpTransport( | 473 void CastStreamingNativeHandler::SetOptionsCastUdpTransport( |
468 const v8::FunctionCallbackInfo<v8::Value>& args) { | 474 const v8::FunctionCallbackInfo<v8::Value>& args) { |
469 CHECK_EQ(2, args.Length()); | 475 CHECK_EQ(2, args.Length()); |
470 CHECK(args[0]->IsInt32()); | 476 CHECK(args[0]->IsInt32()); |
471 CHECK(args[1]->IsObject()); | 477 CHECK(args[1]->IsObject()); |
472 | 478 |
473 const int transport_id = args[0]->ToInt32(args.GetIsolate())->Value(); | 479 const int transport_id = args[0]->ToInt32(args.GetIsolate())->Value(); |
474 CastUdpTransport* transport = GetUdpTransportOrThrow(transport_id); | 480 CastUdpTransport* transport = GetUdpTransportOrThrow(transport_id); |
(...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
609 UdpTransportMap::const_iterator iter = udp_transport_map_.find( | 615 UdpTransportMap::const_iterator iter = udp_transport_map_.find( |
610 transport_id); | 616 transport_id); |
611 if (iter != udp_transport_map_.end()) | 617 if (iter != udp_transport_map_.end()) |
612 return iter->second.get(); | 618 return iter->second.get(); |
613 v8::Isolate* isolate = context()->v8_context()->GetIsolate(); | 619 v8::Isolate* isolate = context()->v8_context()->GetIsolate(); |
614 isolate->ThrowException(v8::Exception::RangeError( | 620 isolate->ThrowException(v8::Exception::RangeError( |
615 v8::String::NewFromUtf8(isolate, kUdpTransportNotFound))); | 621 v8::String::NewFromUtf8(isolate, kUdpTransportNotFound))); |
616 return NULL; | 622 return NULL; |
617 } | 623 } |
618 | 624 |
| 625 bool CastStreamingNativeHandler::FrameReceiverConfigFromArg( |
| 626 v8::Isolate* isolate, |
| 627 const v8::Handle<v8::Value>& arg, |
| 628 media::cast::FrameReceiverConfig* config) { |
| 629 |
| 630 scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); |
| 631 scoped_ptr<base::Value> params_value( |
| 632 converter->FromV8Value(arg, context()->v8_context())); |
| 633 if (!params_value) { |
| 634 isolate->ThrowException(v8::Exception::TypeError( |
| 635 v8::String::NewFromUtf8(isolate, kUnableToConvertParams))); |
| 636 return false; |
| 637 } |
| 638 scoped_ptr<RtpReceiverParams> params = |
| 639 RtpReceiverParams::FromValue(*params_value); |
| 640 if (!params) { |
| 641 isolate->ThrowException(v8::Exception::TypeError( |
| 642 v8::String::NewFromUtf8(isolate, kInvalidRtpParams))); |
| 643 return false; |
| 644 } |
| 645 |
| 646 config->receiver_ssrc = params->receiver_ssrc; |
| 647 config->sender_ssrc = params->sender_ssrc; |
| 648 config->rtp_max_delay_ms = params->max_latency; |
| 649 if (config->rtp_max_delay_ms < 0 || config->rtp_max_delay_ms > 1000) { |
| 650 isolate->ThrowException(v8::Exception::TypeError( |
| 651 v8::String::NewFromUtf8(isolate, kInvalidLatency))); |
| 652 return false; |
| 653 } |
| 654 config->channels = 2; |
| 655 if (params->codec_name == "OPUS") { |
| 656 config->codec = media::cast::CODEC_AUDIO_OPUS; |
| 657 config->rtp_timebase = 48000; |
| 658 config->rtp_payload_type = 127; |
| 659 } else if (params->codec_name == "PCM16") { |
| 660 config->codec = media::cast::CODEC_AUDIO_PCM16; |
| 661 config->rtp_timebase = 48000; |
| 662 config->rtp_payload_type =127; |
| 663 } else if (params->codec_name == "AAC") { |
| 664 config->codec = media::cast::CODEC_AUDIO_AAC; |
| 665 config->rtp_timebase = 48000; |
| 666 config->rtp_payload_type = 127; |
| 667 } else if (params->codec_name == "VP8") { |
| 668 config->codec = media::cast::CODEC_VIDEO_VP8; |
| 669 config->rtp_timebase = 90000; |
| 670 config->rtp_payload_type = 96; |
| 671 } else if (params->codec_name == "H264") { |
| 672 config->codec = media::cast::CODEC_VIDEO_H264; |
| 673 config->rtp_timebase = 90000; |
| 674 config->rtp_payload_type = 96; |
| 675 } |
| 676 if (params->rtp_timebase) { |
| 677 config->rtp_timebase = *params->rtp_timebase; |
| 678 if (config->rtp_timebase < 1000 || config->rtp_timebase > 1000000) { |
| 679 isolate->ThrowException(v8::Exception::TypeError( |
| 680 v8::String::NewFromUtf8(isolate, kInvalidRtpTimebase))); |
| 681 return false; |
| 682 } |
| 683 } |
| 684 if (params->aes_key && |
| 685 !HexDecode(*params->aes_key, &config->aes_key)) { |
| 686 isolate->ThrowException(v8::Exception::Error( |
| 687 v8::String::NewFromUtf8(isolate, kInvalidAesKey))); |
| 688 return false; |
| 689 } |
| 690 if (params->aes_iv_mask && |
| 691 !HexDecode(*params->aes_iv_mask, &config->aes_iv_mask)) { |
| 692 isolate->ThrowException(v8::Exception::Error( |
| 693 v8::String::NewFromUtf8(isolate, kInvalidAesIvMask))); |
| 694 return false; |
| 695 } |
| 696 return true; |
| 697 } |
| 698 |
| 699 bool CastStreamingNativeHandler::IPEndPointFromArg( |
| 700 v8::Isolate* isolate, |
| 701 const v8::Handle<v8::Value>& arg, |
| 702 net::IPEndPoint* ip_endpoint) { |
| 703 scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); |
| 704 scoped_ptr<base::Value> destination_value( |
| 705 converter->FromV8Value(arg, context()->v8_context())); |
| 706 if (!destination_value) { |
| 707 isolate->ThrowException(v8::Exception::TypeError( |
| 708 v8::String::NewFromUtf8(isolate, kInvalidAesIvMask))); |
| 709 return false; |
| 710 } |
| 711 scoped_ptr<IPEndPoint> destination = |
| 712 IPEndPoint::FromValue(*destination_value); |
| 713 if (!destination) { |
| 714 isolate->ThrowException(v8::Exception::TypeError( |
| 715 v8::String::NewFromUtf8(isolate, kInvalidDestination))); |
| 716 return false; |
| 717 } |
| 718 net::IPAddressNumber ip; |
| 719 if (!net::ParseIPLiteralToNumber(destination->address, &ip)) { |
| 720 isolate->ThrowException(v8::Exception::TypeError( |
| 721 v8::String::NewFromUtf8(isolate, kInvalidDestination))); |
| 722 return false; |
| 723 } |
| 724 *ip_endpoint = net::IPEndPoint(ip, destination->port); |
| 725 return true; |
| 726 } |
| 727 |
| 728 void CastStreamingNativeHandler::StartCastRtpReceiver( |
| 729 const v8::FunctionCallbackInfo<v8::Value>& args) { |
| 730 if (args.Length() < 8 || args.Length() > 9 || |
| 731 !args[0]->IsObject() || |
| 732 !args[1]->IsObject() || |
| 733 !args[2]->IsObject() || |
| 734 !args[3]->IsInt32() || |
| 735 !args[4]->IsInt32() || |
| 736 !args[5]->IsNumber() || |
| 737 !args[6]->IsString()) { |
| 738 args.GetIsolate()->ThrowException(v8::Exception::TypeError( |
| 739 v8::String::NewFromUtf8(args.GetIsolate(), kUnableToConvertArgs))); |
| 740 return; |
| 741 } |
| 742 |
| 743 v8::Isolate* isolate = context()->v8_context()->GetIsolate(); |
| 744 |
| 745 scoped_refptr<CastReceiverSession> session( |
| 746 new CastReceiverSession()); |
| 747 media::cast::FrameReceiverConfig audio_config; |
| 748 media::cast::FrameReceiverConfig video_config; |
| 749 net::IPEndPoint local_endpoint; |
| 750 net::IPEndPoint remote_endpoint; |
| 751 |
| 752 if (!FrameReceiverConfigFromArg(isolate, args[0], &audio_config) || |
| 753 !FrameReceiverConfigFromArg(isolate, args[1], &video_config) || |
| 754 !IPEndPointFromArg(isolate, args[2], &local_endpoint)) { |
| 755 return; |
| 756 } |
| 757 |
| 758 const std::string url = *v8::String::Utf8Value(args[7]); |
| 759 blink::WebMediaStream stream = |
| 760 blink::WebMediaStreamRegistry::lookupMediaStreamDescriptor(GURL(url)); |
| 761 |
| 762 if (stream.isNull()) { |
| 763 args.GetIsolate()->ThrowException(v8::Exception::TypeError( |
| 764 v8::String::NewFromUtf8(args.GetIsolate(), kInvalidMediaStreamURL))); |
| 765 return; |
| 766 } |
| 767 |
| 768 const int max_width = args[3]->ToInt32(args.GetIsolate())->Value(); |
| 769 const int max_height = args[4]->ToInt32(args.GetIsolate())->Value(); |
| 770 const double fps = args[5]->NumberValue(); |
| 771 |
| 772 if (fps <= 1) { |
| 773 args.GetIsolate()->ThrowException(v8::Exception::TypeError( |
| 774 v8::String::NewFromUtf8(args.GetIsolate(), kInvalidFPS))); |
| 775 return; |
| 776 } |
| 777 |
| 778 media::VideoCaptureFormat capture_format( |
| 779 gfx::Size(max_width, max_height), |
| 780 fps, |
| 781 media::PIXEL_FORMAT_I420); |
| 782 |
| 783 video_config.target_frame_rate = fps; |
| 784 audio_config.target_frame_rate = 100; |
| 785 |
| 786 media::AudioParameters params( |
| 787 media::AudioParameters::AUDIO_PCM_LINEAR, |
| 788 media::CHANNEL_LAYOUT_STEREO, |
| 789 audio_config.rtp_timebase, // sampling rate |
| 790 16, |
| 791 audio_config.rtp_timebase / audio_config.target_frame_rate); |
| 792 |
| 793 if (!params.IsValid()) { |
| 794 args.GetIsolate()->ThrowException(v8::Exception::TypeError( |
| 795 v8::String::NewFromUtf8(args.GetIsolate(), kInvalidAudioParams))); |
| 796 return; |
| 797 } |
| 798 |
| 799 base::DictionaryValue* options = NULL; |
| 800 if (args.Length() >= 10) { |
| 801 scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create()); |
| 802 base::Value* options_value = |
| 803 converter->FromV8Value(args[8], context()->v8_context()); |
| 804 if (!options_value->IsType(base::Value::TYPE_NULL)) { |
| 805 if (!options_value || !options_value->GetAsDictionary(&options)) { |
| 806 delete options_value; |
| 807 args.GetIsolate()->ThrowException(v8::Exception::TypeError( |
| 808 v8::String::NewFromUtf8(args.GetIsolate(), kUnableToConvertArgs))); |
| 809 return; |
| 810 } |
| 811 } |
| 812 } |
| 813 |
| 814 if (!options) { |
| 815 options = new base::DictionaryValue(); |
| 816 } |
| 817 |
| 818 v8::CopyablePersistentTraits<v8::Function>::CopyablePersistent error_callback; |
| 819 error_callback.Reset(args.GetIsolate(), |
| 820 v8::Handle<v8::Function>(args[7].As<v8::Function>())); |
| 821 |
| 822 session->Start( |
| 823 audio_config, |
| 824 video_config, |
| 825 local_endpoint, |
| 826 remote_endpoint, |
| 827 make_scoped_ptr(options), |
| 828 capture_format, |
| 829 base::Bind(&CastStreamingNativeHandler::AddTracksToMediaStream, |
| 830 weak_factory_.GetWeakPtr(), |
| 831 url, |
| 832 params), |
| 833 base::Bind(&CastStreamingNativeHandler::CallReceiverErrorCallback, |
| 834 weak_factory_.GetWeakPtr(), |
| 835 error_callback)); |
| 836 } |
| 837 |
| 838 void CastStreamingNativeHandler::CallReceiverErrorCallback( |
| 839 v8::CopyablePersistentTraits<v8::Function>::CopyablePersistent function, |
| 840 const std::string& error_message) { |
| 841 v8::Isolate* isolate = context()->v8_context()->GetIsolate(); |
| 842 v8::Handle<v8::Value> arg = v8::String::NewFromUtf8(isolate, |
| 843 error_message.data(), |
| 844 v8::String::kNormalString, |
| 845 error_message.size()); |
| 846 context()->CallFunction( |
| 847 v8::Local<v8::Function>::New(isolate, function), 1, &arg); |
| 848 } |
| 849 |
| 850 |
| 851 void CastStreamingNativeHandler::AddTracksToMediaStream( |
| 852 const std::string& url, |
| 853 const media::AudioParameters& params, |
| 854 scoped_refptr<media::AudioCapturerSource> audio, |
| 855 scoped_ptr<media::VideoCapturerSource> video) { |
| 856 content::AddAudioTrackToMediaStream(audio, params, true, true, url); |
| 857 content::AddVideoTrackToMediaStream(video.Pass(), true, true, url); |
| 858 } |
| 859 |
619 } // namespace extensions | 860 } // namespace extensions |
OLD | NEW |