Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: content/renderer/media/webmediaplayer_impl.cc

Issue 501473003: Move EME code out of WebMediaPlayerImpl. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address CR comments Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 "content/renderer/media/webmediaplayer_impl.h" 5 #include "content/renderer/media/webmediaplayer_impl.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <limits> 8 #include <limits>
9 #include <string> 9 #include <string>
10 #include <vector> 10 #include <vector>
11 11
12 #include "base/bind.h" 12 #include "base/bind.h"
13 #include "base/callback.h" 13 #include "base/callback.h"
14 #include "base/callback_helpers.h" 14 #include "base/callback_helpers.h"
15 #include "base/command_line.h" 15 #include "base/command_line.h"
16 #include "base/debug/alias.h" 16 #include "base/debug/alias.h"
17 #include "base/debug/crash_logging.h" 17 #include "base/debug/crash_logging.h"
18 #include "base/debug/trace_event.h" 18 #include "base/debug/trace_event.h"
19 #include "base/message_loop/message_loop_proxy.h" 19 #include "base/message_loop/message_loop_proxy.h"
20 #include "base/metrics/histogram.h" 20 #include "base/metrics/histogram.h"
21 #include "base/strings/string_number_conversions.h"
22 #include "base/strings/utf_string_conversions.h"
23 #include "base/synchronization/waitable_event.h" 21 #include "base/synchronization/waitable_event.h"
24 #include "cc/blink/web_layer_impl.h" 22 #include "cc/blink/web_layer_impl.h"
25 #include "cc/layers/video_layer.h" 23 #include "cc/layers/video_layer.h"
26 #include "content/public/common/content_switches.h" 24 #include "content/public/common/content_switches.h"
27 #include "content/public/renderer/render_frame.h" 25 #include "content/public/renderer/render_frame.h"
28 #include "content/renderer/media/buffered_data_source.h" 26 #include "content/renderer/media/buffered_data_source.h"
29 #include "content/renderer/media/crypto/key_systems.h" 27 #include "content/renderer/media/crypto/encrypted_media_player_support.h"
30 #include "content/renderer/media/render_media_log.h" 28 #include "content/renderer/media/render_media_log.h"
31 #include "content/renderer/media/texttrack_impl.h" 29 #include "content/renderer/media/texttrack_impl.h"
32 #include "content/renderer/media/webaudiosourceprovider_impl.h" 30 #include "content/renderer/media/webaudiosourceprovider_impl.h"
33 #include "content/renderer/media/webcontentdecryptionmodule_impl.h"
34 #include "content/renderer/media/webinbandtexttrack_impl.h" 31 #include "content/renderer/media/webinbandtexttrack_impl.h"
35 #include "content/renderer/media/webmediaplayer_delegate.h" 32 #include "content/renderer/media/webmediaplayer_delegate.h"
36 #include "content/renderer/media/webmediaplayer_params.h" 33 #include "content/renderer/media/webmediaplayer_params.h"
37 #include "content/renderer/media/webmediaplayer_util.h" 34 #include "content/renderer/media/webmediaplayer_util.h"
38 #include "content/renderer/media/webmediasource_impl.h" 35 #include "content/renderer/media/webmediasource_impl.h"
39 #include "content/renderer/pepper/pepper_webplugin_impl.h"
40 #include "content/renderer/render_thread_impl.h" 36 #include "content/renderer/render_thread_impl.h"
41 #include "gpu/GLES2/gl2extchromium.h" 37 #include "gpu/GLES2/gl2extchromium.h"
42 #include "gpu/command_buffer/common/mailbox_holder.h" 38 #include "gpu/command_buffer/common/mailbox_holder.h"
43 #include "media/audio/null_audio_sink.h" 39 #include "media/audio/null_audio_sink.h"
44 #include "media/base/audio_hardware_config.h" 40 #include "media/base/audio_hardware_config.h"
45 #include "media/base/bind_to_current_loop.h" 41 #include "media/base/bind_to_current_loop.h"
46 #include "media/base/filter_collection.h" 42 #include "media/base/filter_collection.h"
47 #include "media/base/limits.h" 43 #include "media/base/limits.h"
48 #include "media/base/media_log.h" 44 #include "media/base/media_log.h"
49 #include "media/base/media_switches.h" 45 #include "media/base/media_switches.h"
50 #include "media/base/pipeline.h" 46 #include "media/base/pipeline.h"
51 #include "media/base/text_renderer.h" 47 #include "media/base/text_renderer.h"
52 #include "media/base/video_frame.h" 48 #include "media/base/video_frame.h"
53 #include "media/filters/audio_renderer_impl.h" 49 #include "media/filters/audio_renderer_impl.h"
54 #include "media/filters/chunk_demuxer.h" 50 #include "media/filters/chunk_demuxer.h"
55 #include "media/filters/ffmpeg_audio_decoder.h" 51 #include "media/filters/ffmpeg_audio_decoder.h"
56 #include "media/filters/ffmpeg_demuxer.h" 52 #include "media/filters/ffmpeg_demuxer.h"
57 #include "media/filters/ffmpeg_video_decoder.h" 53 #include "media/filters/ffmpeg_video_decoder.h"
58 #include "media/filters/gpu_video_accelerator_factories.h" 54 #include "media/filters/gpu_video_accelerator_factories.h"
59 #include "media/filters/gpu_video_decoder.h" 55 #include "media/filters/gpu_video_decoder.h"
60 #include "media/filters/opus_audio_decoder.h" 56 #include "media/filters/opus_audio_decoder.h"
61 #include "media/filters/video_renderer_impl.h" 57 #include "media/filters/video_renderer_impl.h"
62 #include "media/filters/vpx_video_decoder.h" 58 #include "media/filters/vpx_video_decoder.h"
63 #include "third_party/WebKit/public/platform/WebContentDecryptionModule.h"
64 #include "third_party/WebKit/public/platform/WebContentDecryptionModuleResult.h"
65 #include "third_party/WebKit/public/platform/WebMediaSource.h" 59 #include "third_party/WebKit/public/platform/WebMediaSource.h"
66 #include "third_party/WebKit/public/platform/WebRect.h" 60 #include "third_party/WebKit/public/platform/WebRect.h"
67 #include "third_party/WebKit/public/platform/WebSize.h" 61 #include "third_party/WebKit/public/platform/WebSize.h"
68 #include "third_party/WebKit/public/platform/WebString.h" 62 #include "third_party/WebKit/public/platform/WebString.h"
69 #include "third_party/WebKit/public/platform/WebURL.h" 63 #include "third_party/WebKit/public/platform/WebURL.h"
70 #include "third_party/WebKit/public/web/WebDocument.h"
71 #include "third_party/WebKit/public/web/WebLocalFrame.h" 64 #include "third_party/WebKit/public/web/WebLocalFrame.h"
72 #include "third_party/WebKit/public/web/WebRuntimeFeatures.h"
73 #include "third_party/WebKit/public/web/WebSecurityOrigin.h" 65 #include "third_party/WebKit/public/web/WebSecurityOrigin.h"
74 #include "third_party/WebKit/public/web/WebView.h" 66 #include "third_party/WebKit/public/web/WebView.h"
75 #include "v8/include/v8.h" 67 #include "v8/include/v8.h"
76 68
77 #if defined(ENABLE_PEPPER_CDMS)
78 #include "content/renderer/media/crypto/pepper_cdm_wrapper_impl.h"
79 #endif
80
81 using blink::WebCanvas; 69 using blink::WebCanvas;
82 using blink::WebMediaPlayer; 70 using blink::WebMediaPlayer;
83 using blink::WebRect; 71 using blink::WebRect;
84 using blink::WebSize; 72 using blink::WebSize;
85 using blink::WebString; 73 using blink::WebString;
86 using media::PipelineStatus; 74 using media::PipelineStatus;
87 75
88 namespace { 76 namespace {
89 77
90 // Amount of extra memory used by each player instance reported to V8. 78 // Amount of extra memory used by each player instance reported to V8.
(...skipping 18 matching lines...) Expand all
109 // 97 //
110 // A very slow speed, ie 0.00000001x, causes the machine to lock up. (It seems 98 // A very slow speed, ie 0.00000001x, causes the machine to lock up. (It seems
111 // like a busy loop). It gets unresponsive, although its not completely dead. 99 // like a busy loop). It gets unresponsive, although its not completely dead.
112 // 100 //
113 // Also our timers are not very accurate (especially for ogg), which becomes 101 // Also our timers are not very accurate (especially for ogg), which becomes
114 // evident at low speeds and on Vista. Since other speeds are risky and outside 102 // evident at low speeds and on Vista. Since other speeds are risky and outside
115 // the norms, we think 1/16x to 16x is a safe and useful range for now. 103 // the norms, we think 1/16x to 16x is a safe and useful range for now.
116 const double kMinRate = 0.0625; 104 const double kMinRate = 0.0625;
117 const double kMaxRate = 16.0; 105 const double kMaxRate = 16.0;
118 106
119 // Prefix for histograms related to Encrypted Media Extensions.
120 const char* kMediaEme = "Media.EME.";
121
122 class SyncPointClientImpl : public media::VideoFrame::SyncPointClient { 107 class SyncPointClientImpl : public media::VideoFrame::SyncPointClient {
123 public: 108 public:
124 explicit SyncPointClientImpl( 109 explicit SyncPointClientImpl(
125 blink::WebGraphicsContext3D* web_graphics_context) 110 blink::WebGraphicsContext3D* web_graphics_context)
126 : web_graphics_context_(web_graphics_context) {} 111 : web_graphics_context_(web_graphics_context) {}
127 virtual ~SyncPointClientImpl() {} 112 virtual ~SyncPointClientImpl() {}
128 virtual uint32 InsertSyncPoint() OVERRIDE { 113 virtual uint32 InsertSyncPoint() OVERRIDE {
129 return web_graphics_context_->insertSyncPoint(); 114 return web_graphics_context_->insertSyncPoint();
130 } 115 }
131 virtual void WaitSyncPoint(uint32 sync_point) OVERRIDE { 116 virtual void WaitSyncPoint(uint32 sync_point) OVERRIDE {
132 web_graphics_context_->waitSyncPoint(sync_point); 117 web_graphics_context_->waitSyncPoint(sync_point);
133 } 118 }
134 119
135 private: 120 private:
136 blink::WebGraphicsContext3D* web_graphics_context_; 121 blink::WebGraphicsContext3D* web_graphics_context_;
137 }; 122 };
138 123
139 // Used for calls to decryptor_ready_cb where the result can be ignored.
140 void DoNothing(bool) {
141 }
142
143 } // namespace 124 } // namespace
144 125
145 namespace content { 126 namespace content {
146 127
147 class BufferedDataSourceHostImpl; 128 class BufferedDataSourceHostImpl;
148 129
149 #define COMPILE_ASSERT_MATCHING_ENUM(name) \ 130 #define COMPILE_ASSERT_MATCHING_ENUM(name) \
150 COMPILE_ASSERT(static_cast<int>(WebMediaPlayer::CORSMode ## name) == \ 131 COMPILE_ASSERT(static_cast<int>(WebMediaPlayer::CORSMode ## name) == \
151 static_cast<int>(BufferedResourceLoader::k ## name), \ 132 static_cast<int>(BufferedResourceLoader::k ## name), \
152 mismatching_enums) 133 mismatching_enums)
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
199 chunk_demuxer_(NULL), 180 chunk_demuxer_(NULL),
200 // Threaded compositing isn't enabled universally yet. 181 // Threaded compositing isn't enabled universally yet.
201 compositor_task_runner_( 182 compositor_task_runner_(
202 RenderThreadImpl::current()->compositor_message_loop_proxy() 183 RenderThreadImpl::current()->compositor_message_loop_proxy()
203 ? RenderThreadImpl::current()->compositor_message_loop_proxy() 184 ? RenderThreadImpl::current()->compositor_message_loop_proxy()
204 : base::MessageLoopProxy::current()), 185 : base::MessageLoopProxy::current()),
205 compositor_(new VideoFrameCompositor( 186 compositor_(new VideoFrameCompositor(
206 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnNaturalSizeChanged), 187 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnNaturalSizeChanged),
207 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnOpacityChanged))), 188 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnOpacityChanged))),
208 text_track_index_(0), 189 text_track_index_(0),
209 web_cdm_(NULL) { 190 encrypted_media_support_(EncryptedMediaPlayerSupport::create(client)) {
xhwang 2014/08/23 00:31:57 DCHECK(encrypted_media_support_)?
acolwell GONE FROM CHROMIUM 2014/08/25 18:10:43 Done.
210 media_log_->AddEvent( 191 media_log_->AddEvent(
211 media_log_->CreateEvent(media::MediaLogEvent::WEBMEDIAPLAYER_CREATED)); 192 media_log_->CreateEvent(media::MediaLogEvent::WEBMEDIAPLAYER_CREATED));
212 193
213 // |gpu_factories_| requires that its entry points be called on its 194 // |gpu_factories_| requires that its entry points be called on its
214 // |GetTaskRunner()|. Since |pipeline_| will own decoders created from the 195 // |GetTaskRunner()|. Since |pipeline_| will own decoders created from the
215 // factories, require that their message loops are identical. 196 // factories, require that their message loops are identical.
216 DCHECK(!gpu_factories_ || (gpu_factories_->GetTaskRunner() == media_loop_)); 197 DCHECK(!gpu_factories_ || (gpu_factories_->GetTaskRunner() == media_loop_));
217 198
218 // Let V8 know we started new thread if we did not do it yet. 199 // Let V8 know we started new thread if we did not do it yet.
219 // Made separate task to avoid deletion of player currently being created. 200 // Made separate task to avoid deletion of player currently being created.
(...skipping 429 matching lines...) Expand 10 before | Expand all | Expand 10 after
649 false); 630 false);
650 631
651 web_graphics_context->deleteTexture(source_texture); 632 web_graphics_context->deleteTexture(source_texture);
652 web_graphics_context->flush(); 633 web_graphics_context->flush();
653 634
654 SyncPointClientImpl client(web_graphics_context); 635 SyncPointClientImpl client(web_graphics_context);
655 video_frame->UpdateReleaseSyncPoint(&client); 636 video_frame->UpdateReleaseSyncPoint(&client);
656 return true; 637 return true;
657 } 638 }
658 639
659 // Helper functions to report media EME related stats to UMA. They follow the
660 // convention of more commonly used macros UMA_HISTOGRAM_ENUMERATION and
661 // UMA_HISTOGRAM_COUNTS. The reason that we cannot use those macros directly is
662 // that UMA_* macros require the names to be constant throughout the process'
663 // lifetime.
664 static void EmeUMAHistogramEnumeration(const std::string& key_system,
665 const std::string& method,
666 int sample,
667 int boundary_value) {
668 base::LinearHistogram::FactoryGet(
669 kMediaEme + KeySystemNameForUMA(key_system) + "." + method,
670 1, boundary_value, boundary_value + 1,
671 base::Histogram::kUmaTargetedHistogramFlag)->Add(sample);
672 }
673
674 static void EmeUMAHistogramCounts(const std::string& key_system,
675 const std::string& method,
676 int sample) {
677 // Use the same parameters as UMA_HISTOGRAM_COUNTS.
678 base::Histogram::FactoryGet(
679 kMediaEme + KeySystemNameForUMA(key_system) + "." + method,
680 1, 1000000, 50, base::Histogram::kUmaTargetedHistogramFlag)->Add(sample);
681 }
682
683 // Helper enum for reporting generateKeyRequest/addKey histograms.
684 enum MediaKeyException {
685 kUnknownResultId,
686 kSuccess,
687 kKeySystemNotSupported,
688 kInvalidPlayerState,
689 kMaxMediaKeyException
690 };
691
692 static MediaKeyException MediaKeyExceptionForUMA(
693 WebMediaPlayer::MediaKeyException e) {
694 switch (e) {
695 case WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported:
696 return kKeySystemNotSupported;
697 case WebMediaPlayer::MediaKeyExceptionInvalidPlayerState:
698 return kInvalidPlayerState;
699 case WebMediaPlayer::MediaKeyExceptionNoError:
700 return kSuccess;
701 default:
702 return kUnknownResultId;
703 }
704 }
705
706 // Helper for converting |key_system| name and exception |e| to a pair of enum
707 // values from above, for reporting to UMA.
708 static void ReportMediaKeyExceptionToUMA(const std::string& method,
709 const std::string& key_system,
710 WebMediaPlayer::MediaKeyException e) {
711 MediaKeyException result_id = MediaKeyExceptionForUMA(e);
712 DCHECK_NE(result_id, kUnknownResultId) << e;
713 EmeUMAHistogramEnumeration(
714 key_system, method, result_id, kMaxMediaKeyException);
715 }
716
717 // Convert a WebString to ASCII, falling back on an empty string in the case
718 // of a non-ASCII string.
719 static std::string ToASCIIOrEmpty(const blink::WebString& string) {
720 return base::IsStringASCII(string) ? base::UTF16ToASCII(string)
721 : std::string();
722 }
723
724 WebMediaPlayer::MediaKeyException 640 WebMediaPlayer::MediaKeyException
725 WebMediaPlayerImpl::generateKeyRequest(const WebString& key_system, 641 WebMediaPlayerImpl::generateKeyRequest(const WebString& key_system,
726 const unsigned char* init_data, 642 const unsigned char* init_data,
727 unsigned init_data_length) { 643 unsigned init_data_length) {
728 DVLOG(1) << "generateKeyRequest: " << base::string16(key_system) << ": "
729 << std::string(reinterpret_cast<const char*>(init_data),
730 static_cast<size_t>(init_data_length));
731
732 std::string ascii_key_system =
733 GetUnprefixedKeySystemName(ToASCIIOrEmpty(key_system));
734
735 WebMediaPlayer::MediaKeyException e =
736 GenerateKeyRequestInternal(ascii_key_system, init_data, init_data_length);
737 ReportMediaKeyExceptionToUMA("generateKeyRequest", ascii_key_system, e);
738 return e;
739 }
740
741 // Guess the type of |init_data|. This is only used to handle some corner cases
742 // so we keep it as simple as possible without breaking major use cases.
743 static std::string GuessInitDataType(const unsigned char* init_data,
744 unsigned init_data_length) {
745 // Most WebM files use KeyId of 16 bytes. MP4 init data are always >16 bytes.
746 if (init_data_length == 16)
747 return "video/webm";
748
749 return "video/mp4";
750 }
751
752 WebMediaPlayer::MediaKeyException
753 WebMediaPlayerImpl::GenerateKeyRequestInternal(const std::string& key_system,
754 const unsigned char* init_data,
755 unsigned init_data_length) {
756 DCHECK(main_loop_->BelongsToCurrentThread()); 644 DCHECK(main_loop_->BelongsToCurrentThread());
757 645
758 if (!IsConcreteSupportedKeySystem(key_system)) 646 return encrypted_media_support_->GenerateKeyRequest(
759 return WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported; 647 frame_, key_system, init_data, init_data_length);
760
761 // We do not support run-time switching between key systems for now.
762 if (current_key_system_.empty()) {
763 if (!proxy_decryptor_) {
764 proxy_decryptor_.reset(new ProxyDecryptor(
765 #if defined(ENABLE_PEPPER_CDMS)
766 // Create() must be called synchronously as |frame_| may not be
767 // valid afterwards.
768 base::Bind(&PepperCdmWrapperImpl::Create, frame_),
769 #elif defined(ENABLE_BROWSER_CDMS)
770 #error Browser side CDM in WMPI for prefixed EME API not supported yet.
771 #endif
772 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnKeyAdded),
773 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnKeyError),
774 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnKeyMessage)));
775 }
776
777 GURL security_origin(frame_->document().securityOrigin().toString());
778 if (!proxy_decryptor_->InitializeCDM(key_system, security_origin))
779 return WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported;
780
781 if (proxy_decryptor_ && !decryptor_ready_cb_.is_null()) {
782 base::ResetAndReturn(&decryptor_ready_cb_)
783 .Run(proxy_decryptor_->GetDecryptor(), base::Bind(DoNothing));
784 }
785
786 current_key_system_ = key_system;
787 } else if (key_system != current_key_system_) {
788 return WebMediaPlayer::MediaKeyExceptionInvalidPlayerState;
789 }
790
791 std::string init_data_type = init_data_type_;
792 if (init_data_type.empty())
793 init_data_type = GuessInitDataType(init_data, init_data_length);
794
795 // TODO(xhwang): We assume all streams are from the same container (thus have
796 // the same "type") for now. In the future, the "type" should be passed down
797 // from the application.
798 if (!proxy_decryptor_->GenerateKeyRequest(
799 init_data_type, init_data, init_data_length)) {
800 current_key_system_.clear();
801 return WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported;
802 }
803
804 return WebMediaPlayer::MediaKeyExceptionNoError;
805 } 648 }
806 649
807 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::addKey( 650 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::addKey(
808 const WebString& key_system, 651 const WebString& key_system,
809 const unsigned char* key, 652 const unsigned char* key,
810 unsigned key_length, 653 unsigned key_length,
811 const unsigned char* init_data, 654 const unsigned char* init_data,
812 unsigned init_data_length, 655 unsigned init_data_length,
813 const WebString& session_id) { 656 const WebString& session_id) {
814 DVLOG(1) << "addKey: " << base::string16(key_system) << ": " 657 DCHECK(main_loop_->BelongsToCurrentThread());
815 << std::string(reinterpret_cast<const char*>(key),
816 static_cast<size_t>(key_length)) << ", "
817 << std::string(reinterpret_cast<const char*>(init_data),
818 static_cast<size_t>(init_data_length)) << " ["
819 << base::string16(session_id) << "]";
820 658
821 std::string ascii_key_system = 659 return encrypted_media_support_->AddKey(
822 GetUnprefixedKeySystemName(ToASCIIOrEmpty(key_system)); 660 key_system, key, key_length, init_data, init_data_length, session_id);
823 std::string ascii_session_id = ToASCIIOrEmpty(session_id);
824
825 WebMediaPlayer::MediaKeyException e = AddKeyInternal(ascii_key_system,
826 key,
827 key_length,
828 init_data,
829 init_data_length,
830 ascii_session_id);
831 ReportMediaKeyExceptionToUMA("addKey", ascii_key_system, e);
832 return e;
833 }
834
835 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::AddKeyInternal(
836 const std::string& key_system,
837 const unsigned char* key,
838 unsigned key_length,
839 const unsigned char* init_data,
840 unsigned init_data_length,
841 const std::string& session_id) {
842 DCHECK(key);
843 DCHECK_GT(key_length, 0u);
844
845 if (!IsConcreteSupportedKeySystem(key_system))
846 return WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported;
847
848 if (current_key_system_.empty() || key_system != current_key_system_)
849 return WebMediaPlayer::MediaKeyExceptionInvalidPlayerState;
850
851 proxy_decryptor_->AddKey(
852 key, key_length, init_data, init_data_length, session_id);
853 return WebMediaPlayer::MediaKeyExceptionNoError;
854 } 661 }
855 662
856 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::cancelKeyRequest( 663 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::cancelKeyRequest(
857 const WebString& key_system, 664 const WebString& key_system,
858 const WebString& session_id) { 665 const WebString& session_id) {
859 DVLOG(1) << "cancelKeyRequest: " << base::string16(key_system) << ": " 666 DCHECK(main_loop_->BelongsToCurrentThread());
860 << " [" << base::string16(session_id) << "]";
861 667
862 std::string ascii_key_system = 668 return encrypted_media_support_->CancelKeyRequest(key_system, session_id);
863 GetUnprefixedKeySystemName(ToASCIIOrEmpty(key_system));
864 std::string ascii_session_id = ToASCIIOrEmpty(session_id);
865
866 WebMediaPlayer::MediaKeyException e =
867 CancelKeyRequestInternal(ascii_key_system, ascii_session_id);
868 ReportMediaKeyExceptionToUMA("cancelKeyRequest", ascii_key_system, e);
869 return e;
870 }
871
872 WebMediaPlayer::MediaKeyException WebMediaPlayerImpl::CancelKeyRequestInternal(
873 const std::string& key_system,
874 const std::string& session_id) {
875 if (!IsConcreteSupportedKeySystem(key_system))
876 return WebMediaPlayer::MediaKeyExceptionKeySystemNotSupported;
877
878 if (current_key_system_.empty() || key_system != current_key_system_)
879 return WebMediaPlayer::MediaKeyExceptionInvalidPlayerState;
880
881 proxy_decryptor_->CancelKeyRequest(session_id);
882 return WebMediaPlayer::MediaKeyExceptionNoError;
883 } 669 }
884 670
885 void WebMediaPlayerImpl::setContentDecryptionModule( 671 void WebMediaPlayerImpl::setContentDecryptionModule(
886 blink::WebContentDecryptionModule* cdm) { 672 blink::WebContentDecryptionModule* cdm) {
887 DCHECK(main_loop_->BelongsToCurrentThread()); 673 DCHECK(main_loop_->BelongsToCurrentThread());
888 674
889 // TODO(xhwang): Support setMediaKeys(0) if necessary: http://crbug.com/330324 675 encrypted_media_support_->SetContentDecryptionModule(cdm);
890 if (!cdm)
891 return;
892
893 web_cdm_ = ToWebContentDecryptionModuleImpl(cdm);
894
895 if (web_cdm_ && !decryptor_ready_cb_.is_null())
896 base::ResetAndReturn(&decryptor_ready_cb_)
897 .Run(web_cdm_->GetDecryptor(), base::Bind(DoNothing));
898 } 676 }
899 677
900 void WebMediaPlayerImpl::setContentDecryptionModule( 678 void WebMediaPlayerImpl::setContentDecryptionModule(
901 blink::WebContentDecryptionModule* cdm, 679 blink::WebContentDecryptionModule* cdm,
902 blink::WebContentDecryptionModuleResult result) { 680 blink::WebContentDecryptionModuleResult result) {
903 DCHECK(main_loop_->BelongsToCurrentThread()); 681 DCHECK(main_loop_->BelongsToCurrentThread());
904 682
905 // TODO(xhwang): Support setMediaKeys(0) if necessary: http://crbug.com/330324 683 encrypted_media_support_->SetContentDecryptionModule(cdm, result);
906 if (!cdm) {
907 result.completeWithError(
908 blink::WebContentDecryptionModuleExceptionNotSupportedError,
909 0,
910 "Null MediaKeys object is not supported.");
911 return;
912 }
913
914 web_cdm_ = ToWebContentDecryptionModuleImpl(cdm);
915
916 if (web_cdm_ && !decryptor_ready_cb_.is_null()) {
917 base::ResetAndReturn(&decryptor_ready_cb_)
918 .Run(web_cdm_->GetDecryptor(),
919 BIND_TO_RENDER_LOOP1(
920 &WebMediaPlayerImpl::ContentDecryptionModuleAttached, result));
921 } else {
922 // No pipeline/decoder connected, so resolve the promise. When something
923 // is connected, setting the CDM will happen in SetDecryptorReadyCB().
924 ContentDecryptionModuleAttached(result, true);
925 }
926 } 684 }
927 685
928 void WebMediaPlayerImpl::setContentDecryptionModuleSync( 686 void WebMediaPlayerImpl::setContentDecryptionModuleSync(
929 blink::WebContentDecryptionModule* cdm) { 687 blink::WebContentDecryptionModule* cdm) {
930 DCHECK(main_loop_->BelongsToCurrentThread()); 688 DCHECK(main_loop_->BelongsToCurrentThread());
931 689
932 // Used when loading media and no pipeline/decoder attached yet. 690 encrypted_media_support_->SetContentDecryptionModuleSync(cdm);
933 DCHECK(decryptor_ready_cb_.is_null());
934
935 web_cdm_ = ToWebContentDecryptionModuleImpl(cdm);
936 }
937
938 void WebMediaPlayerImpl::ContentDecryptionModuleAttached(
939 blink::WebContentDecryptionModuleResult result,
940 bool success) {
941 if (success) {
942 result.complete();
943 return;
944 }
945
946 result.completeWithError(
947 blink::WebContentDecryptionModuleExceptionNotSupportedError,
948 0,
949 "Unable to set MediaKeys object");
950 } 691 }
951 692
952 void WebMediaPlayerImpl::OnPipelineSeeked(bool time_changed, 693 void WebMediaPlayerImpl::OnPipelineSeeked(bool time_changed,
953 PipelineStatus status) { 694 PipelineStatus status) {
954 DVLOG(1) << __FUNCTION__ << "(" << time_changed << ", " << status << ")"; 695 DVLOG(1) << __FUNCTION__ << "(" << time_changed << ", " << status << ")";
955 DCHECK(main_loop_->BelongsToCurrentThread()); 696 DCHECK(main_loop_->BelongsToCurrentThread());
956 seeking_ = false; 697 seeking_ = false;
957 if (pending_seek_) { 698 if (pending_seek_) {
958 pending_seek_ = false; 699 pending_seek_ = false;
959 seek(pending_seek_seconds_); 700 seek(pending_seek_seconds_);
(...skipping 25 matching lines...) Expand all
985 if (ready_state_ == WebMediaPlayer::ReadyStateHaveNothing) { 726 if (ready_state_ == WebMediaPlayer::ReadyStateHaveNothing) {
986 // Any error that occurs before reaching ReadyStateHaveMetadata should 727 // Any error that occurs before reaching ReadyStateHaveMetadata should
987 // be considered a format error. 728 // be considered a format error.
988 SetNetworkState(WebMediaPlayer::NetworkStateFormatError); 729 SetNetworkState(WebMediaPlayer::NetworkStateFormatError);
989 return; 730 return;
990 } 731 }
991 732
992 SetNetworkState(PipelineErrorToNetworkState(error)); 733 SetNetworkState(PipelineErrorToNetworkState(error));
993 734
994 if (error == media::PIPELINE_ERROR_DECRYPT) 735 if (error == media::PIPELINE_ERROR_DECRYPT)
995 EmeUMAHistogramCounts(current_key_system_, "DecryptError", 1); 736 encrypted_media_support_->OnPipelineDecryptError();
xhwang 2014/08/23 00:31:57 PIPELINE_ERROR_DECRYPT is pretty much deprecated.
ddorwin 2014/08/25 17:10:14 MEDIA_ERR_DECRYPT is dead, but I don't know if thi
acolwell GONE FROM CHROMIUM 2014/08/25 18:10:43 It looks like the VideoRendererImpl still generate
996 } 737 }
997 738
998 void WebMediaPlayerImpl::OnPipelineMetadata( 739 void WebMediaPlayerImpl::OnPipelineMetadata(
999 media::PipelineMetadata metadata) { 740 media::PipelineMetadata metadata) {
1000 DVLOG(1) << __FUNCTION__; 741 DVLOG(1) << __FUNCTION__;
1001 742
1002 pipeline_metadata_ = metadata; 743 pipeline_metadata_ = metadata;
1003 744
1004 UMA_HISTOGRAM_ENUMERATION("Media.VideoRotation", 745 UMA_HISTOGRAM_ENUMERATION("Media.VideoRotation",
1005 metadata.video_rotation, 746 metadata.video_rotation,
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
1040 if (should_notify_time_changed_) 781 if (should_notify_time_changed_)
1041 client_->timeChanged(); 782 client_->timeChanged();
1042 } 783 }
1043 784
1044 void WebMediaPlayerImpl::OnDemuxerOpened() { 785 void WebMediaPlayerImpl::OnDemuxerOpened() {
1045 DCHECK(main_loop_->BelongsToCurrentThread()); 786 DCHECK(main_loop_->BelongsToCurrentThread());
1046 client_->mediaSourceOpened(new WebMediaSourceImpl( 787 client_->mediaSourceOpened(new WebMediaSourceImpl(
1047 chunk_demuxer_, base::Bind(&LogMediaSourceError, media_log_))); 788 chunk_demuxer_, base::Bind(&LogMediaSourceError, media_log_)));
1048 } 789 }
1049 790
1050 void WebMediaPlayerImpl::OnKeyAdded(const std::string& session_id) {
1051 DCHECK(main_loop_->BelongsToCurrentThread());
1052 EmeUMAHistogramCounts(current_key_system_, "KeyAdded", 1);
1053 client_->keyAdded(
1054 WebString::fromUTF8(GetPrefixedKeySystemName(current_key_system_)),
1055 WebString::fromUTF8(session_id));
1056 }
1057
1058 void WebMediaPlayerImpl::OnNeedKey(const std::string& type,
1059 const std::vector<uint8>& init_data) {
1060 DCHECK(main_loop_->BelongsToCurrentThread());
1061
1062 // Do not fire NeedKey event if encrypted media is not enabled.
1063 if (!blink::WebRuntimeFeatures::isPrefixedEncryptedMediaEnabled() &&
1064 !blink::WebRuntimeFeatures::isEncryptedMediaEnabled()) {
1065 return;
1066 }
1067
1068 UMA_HISTOGRAM_COUNTS(kMediaEme + std::string("NeedKey"), 1);
1069
1070 DCHECK(init_data_type_.empty() || type.empty() || type == init_data_type_);
1071 if (init_data_type_.empty())
1072 init_data_type_ = type;
1073
1074 const uint8* init_data_ptr = init_data.empty() ? NULL : &init_data[0];
1075 client_->keyNeeded(
1076 WebString::fromUTF8(type), init_data_ptr, init_data.size());
1077 }
1078
1079 void WebMediaPlayerImpl::OnAddTextTrack( 791 void WebMediaPlayerImpl::OnAddTextTrack(
1080 const media::TextTrackConfig& config, 792 const media::TextTrackConfig& config,
1081 const media::AddTextTrackDoneCB& done_cb) { 793 const media::AddTextTrackDoneCB& done_cb) {
1082 DCHECK(main_loop_->BelongsToCurrentThread()); 794 DCHECK(main_loop_->BelongsToCurrentThread());
1083 795
1084 const WebInbandTextTrackImpl::Kind web_kind = 796 const WebInbandTextTrackImpl::Kind web_kind =
1085 static_cast<WebInbandTextTrackImpl::Kind>(config.kind()); 797 static_cast<WebInbandTextTrackImpl::Kind>(config.kind());
1086 const blink::WebString web_label = 798 const blink::WebString web_label =
1087 blink::WebString::fromUTF8(config.label()); 799 blink::WebString::fromUTF8(config.label());
1088 const blink::WebString web_language = 800 const blink::WebString web_language =
1089 blink::WebString::fromUTF8(config.language()); 801 blink::WebString::fromUTF8(config.language());
1090 const blink::WebString web_id = 802 const blink::WebString web_id =
1091 blink::WebString::fromUTF8(config.id()); 803 blink::WebString::fromUTF8(config.id());
1092 804
1093 scoped_ptr<WebInbandTextTrackImpl> web_inband_text_track( 805 scoped_ptr<WebInbandTextTrackImpl> web_inband_text_track(
1094 new WebInbandTextTrackImpl(web_kind, web_label, web_language, web_id, 806 new WebInbandTextTrackImpl(web_kind, web_label, web_language, web_id,
1095 text_track_index_++)); 807 text_track_index_++));
1096 808
1097 scoped_ptr<media::TextTrack> text_track( 809 scoped_ptr<media::TextTrack> text_track(
1098 new TextTrackImpl(main_loop_, client_, web_inband_text_track.Pass())); 810 new TextTrackImpl(main_loop_, client_, web_inband_text_track.Pass()));
1099 811
1100 done_cb.Run(text_track.Pass()); 812 done_cb.Run(text_track.Pass());
1101 } 813 }
1102 814
1103 void WebMediaPlayerImpl::OnKeyError(const std::string& session_id,
1104 media::MediaKeys::KeyError error_code,
1105 uint32 system_code) {
1106 DCHECK(main_loop_->BelongsToCurrentThread());
1107
1108 EmeUMAHistogramEnumeration(current_key_system_, "KeyError",
1109 error_code, media::MediaKeys::kMaxKeyError);
1110
1111 unsigned short short_system_code = 0;
1112 if (system_code > std::numeric_limits<unsigned short>::max()) {
1113 LOG(WARNING) << "system_code exceeds unsigned short limit.";
1114 short_system_code = std::numeric_limits<unsigned short>::max();
1115 } else {
1116 short_system_code = static_cast<unsigned short>(system_code);
1117 }
1118
1119 client_->keyError(
1120 WebString::fromUTF8(GetPrefixedKeySystemName(current_key_system_)),
1121 WebString::fromUTF8(session_id),
1122 static_cast<blink::WebMediaPlayerClient::MediaKeyErrorCode>(error_code),
1123 short_system_code);
1124 }
1125
1126 void WebMediaPlayerImpl::OnKeyMessage(const std::string& session_id,
1127 const std::vector<uint8>& message,
1128 const GURL& destination_url) {
1129 DCHECK(main_loop_->BelongsToCurrentThread());
1130
1131 DCHECK(destination_url.is_empty() || destination_url.is_valid());
1132
1133 client_->keyMessage(
1134 WebString::fromUTF8(GetPrefixedKeySystemName(current_key_system_)),
1135 WebString::fromUTF8(session_id),
1136 message.empty() ? NULL : &message[0],
1137 message.size(),
1138 destination_url);
1139 }
1140
1141 void WebMediaPlayerImpl::DataSourceInitialized(bool success) { 815 void WebMediaPlayerImpl::DataSourceInitialized(bool success) {
1142 DCHECK(main_loop_->BelongsToCurrentThread()); 816 DCHECK(main_loop_->BelongsToCurrentThread());
1143 817
1144 if (!success) { 818 if (!success) {
1145 SetNetworkState(WebMediaPlayer::NetworkStateFormatError); 819 SetNetworkState(WebMediaPlayer::NetworkStateFormatError);
1146 return; 820 return;
1147 } 821 }
1148 822
1149 StartPipeline(); 823 StartPipeline();
1150 } 824 }
(...skipping 11 matching lines...) Expand all
1162 836
1163 void WebMediaPlayerImpl::StartPipeline() { 837 void WebMediaPlayerImpl::StartPipeline() {
1164 DCHECK(main_loop_->BelongsToCurrentThread()); 838 DCHECK(main_loop_->BelongsToCurrentThread());
1165 const CommandLine* cmd_line = CommandLine::ForCurrentProcess(); 839 const CommandLine* cmd_line = CommandLine::ForCurrentProcess();
1166 840
1167 // Keep track if this is a MSE or non-MSE playback. 841 // Keep track if this is a MSE or non-MSE playback.
1168 UMA_HISTOGRAM_BOOLEAN("Media.MSE.Playback", 842 UMA_HISTOGRAM_BOOLEAN("Media.MSE.Playback",
1169 (load_type_ == LoadTypeMediaSource)); 843 (load_type_ == LoadTypeMediaSource));
1170 844
1171 media::LogCB mse_log_cb; 845 media::LogCB mse_log_cb;
846 media::Demuxer::NeedKeyCB need_key_cb =
847 encrypted_media_support_->CreateNeedKeyCB();
xhwang 2014/08/23 00:31:57 FYI, we should be able to pass the need key cb in
acolwell GONE FROM CHROMIUM 2014/08/25 18:10:43 Acknowledged.
1172 848
1173 // Figure out which demuxer to use. 849 // Figure out which demuxer to use.
1174 if (load_type_ != LoadTypeMediaSource) { 850 if (load_type_ != LoadTypeMediaSource) {
1175 DCHECK(!chunk_demuxer_); 851 DCHECK(!chunk_demuxer_);
1176 DCHECK(data_source_); 852 DCHECK(data_source_);
1177 853
1178 demuxer_.reset(new media::FFmpegDemuxer( 854 demuxer_.reset(new media::FFmpegDemuxer(
1179 media_loop_, data_source_.get(), 855 media_loop_, data_source_.get(),
1180 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnNeedKey), 856 need_key_cb,
1181 media_log_)); 857 media_log_));
1182 } else { 858 } else {
1183 DCHECK(!chunk_demuxer_); 859 DCHECK(!chunk_demuxer_);
1184 DCHECK(!data_source_); 860 DCHECK(!data_source_);
1185 861
1186 mse_log_cb = base::Bind(&LogMediaSourceError, media_log_); 862 mse_log_cb = base::Bind(&LogMediaSourceError, media_log_);
1187 863
1188 chunk_demuxer_ = new media::ChunkDemuxer( 864 chunk_demuxer_ = new media::ChunkDemuxer(
1189 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnDemuxerOpened), 865 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnDemuxerOpened),
1190 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnNeedKey), 866 need_key_cb,
1191 mse_log_cb, 867 mse_log_cb,
1192 true); 868 true);
1193 demuxer_.reset(chunk_demuxer_); 869 demuxer_.reset(chunk_demuxer_);
1194 } 870 }
1195 871
1196 scoped_ptr<media::FilterCollection> filter_collection( 872 scoped_ptr<media::FilterCollection> filter_collection(
1197 new media::FilterCollection()); 873 new media::FilterCollection());
1198 filter_collection->SetDemuxer(demuxer_.get()); 874 filter_collection->SetDemuxer(demuxer_.get());
1199 875
1200 media::SetDecryptorReadyCB set_decryptor_ready_cb = 876 media::SetDecryptorReadyCB set_decryptor_ready_cb =
1201 BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::SetDecryptorReadyCB); 877 encrypted_media_support_->CreateSetDecryptorReadyCB();
1202 878
1203 // Create our audio decoders and renderer. 879 // Create our audio decoders and renderer.
1204 ScopedVector<media::AudioDecoder> audio_decoders; 880 ScopedVector<media::AudioDecoder> audio_decoders;
1205 audio_decoders.push_back(new media::FFmpegAudioDecoder(media_loop_, 881 audio_decoders.push_back(new media::FFmpegAudioDecoder(media_loop_,
1206 mse_log_cb)); 882 mse_log_cb));
1207 audio_decoders.push_back(new media::OpusAudioDecoder(media_loop_)); 883 audio_decoders.push_back(new media::OpusAudioDecoder(media_loop_));
1208 884
1209 scoped_ptr<media::AudioRenderer> audio_renderer(new media::AudioRendererImpl( 885 scoped_ptr<media::AudioRenderer> audio_renderer(new media::AudioRendererImpl(
1210 media_loop_, 886 media_loop_,
1211 audio_source_provider_.get(), 887 audio_source_provider_.get(),
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after
1332 1008
1333 void WebMediaPlayerImpl::FrameReady( 1009 void WebMediaPlayerImpl::FrameReady(
1334 const scoped_refptr<media::VideoFrame>& frame) { 1010 const scoped_refptr<media::VideoFrame>& frame) {
1335 compositor_task_runner_->PostTask( 1011 compositor_task_runner_->PostTask(
1336 FROM_HERE, 1012 FROM_HERE,
1337 base::Bind(&VideoFrameCompositor::UpdateCurrentFrame, 1013 base::Bind(&VideoFrameCompositor::UpdateCurrentFrame,
1338 base::Unretained(compositor_), 1014 base::Unretained(compositor_),
1339 frame)); 1015 frame));
1340 } 1016 }
1341 1017
1342 void WebMediaPlayerImpl::SetDecryptorReadyCB(
1343 const media::DecryptorReadyCB& decryptor_ready_cb) {
1344 DCHECK(main_loop_->BelongsToCurrentThread());
1345
1346 // Cancels the previous decryptor request.
1347 if (decryptor_ready_cb.is_null()) {
1348 if (!decryptor_ready_cb_.is_null()) {
1349 base::ResetAndReturn(&decryptor_ready_cb_)
1350 .Run(NULL, base::Bind(DoNothing));
1351 }
1352 return;
1353 }
1354
1355 // TODO(xhwang): Support multiple decryptor notification request (e.g. from
1356 // video and audio). The current implementation is okay for the current
1357 // media pipeline since we initialize audio and video decoders in sequence.
1358 // But WebMediaPlayerImpl should not depend on media pipeline's implementation
1359 // detail.
1360 DCHECK(decryptor_ready_cb_.is_null());
1361
1362 // Mixed use of prefixed and unprefixed EME APIs is disallowed by Blink.
1363 DCHECK(!proxy_decryptor_ || !web_cdm_);
1364
1365 if (proxy_decryptor_) {
1366 decryptor_ready_cb.Run(proxy_decryptor_->GetDecryptor(),
1367 base::Bind(DoNothing));
1368 return;
1369 }
1370
1371 if (web_cdm_) {
1372 decryptor_ready_cb.Run(web_cdm_->GetDecryptor(), base::Bind(DoNothing));
1373 return;
1374 }
1375
1376 decryptor_ready_cb_ = decryptor_ready_cb;
1377 }
1378
1379 static void GetCurrentFrameAndSignal( 1018 static void GetCurrentFrameAndSignal(
1380 VideoFrameCompositor* compositor, 1019 VideoFrameCompositor* compositor,
1381 scoped_refptr<media::VideoFrame>* video_frame_out, 1020 scoped_refptr<media::VideoFrame>* video_frame_out,
1382 base::WaitableEvent* event) { 1021 base::WaitableEvent* event) {
1383 TRACE_EVENT0("media", "GetCurrentFrameAndSignal"); 1022 TRACE_EVENT0("media", "GetCurrentFrameAndSignal");
1384 *video_frame_out = compositor->GetCurrentFrame(); 1023 *video_frame_out = compositor->GetCurrentFrame();
1385 event->Signal(); 1024 event->Signal();
1386 } 1025 }
1387 1026
1388 scoped_refptr<media::VideoFrame> 1027 scoped_refptr<media::VideoFrame>
1389 WebMediaPlayerImpl::GetCurrentFrameFromCompositor() { 1028 WebMediaPlayerImpl::GetCurrentFrameFromCompositor() {
1390 TRACE_EVENT0("media", "WebMediaPlayerImpl::GetCurrentFrameFromCompositor"); 1029 TRACE_EVENT0("media", "WebMediaPlayerImpl::GetCurrentFrameFromCompositor");
1391 if (compositor_task_runner_->BelongsToCurrentThread()) 1030 if (compositor_task_runner_->BelongsToCurrentThread())
1392 return compositor_->GetCurrentFrame(); 1031 return compositor_->GetCurrentFrame();
1393 1032
1394 // Use a posted task and waitable event instead of a lock otherwise 1033 // Use a posted task and waitable event instead of a lock otherwise
1395 // WebGL/Canvas can see different content than what the compositor is seeing. 1034 // WebGL/Canvas can see different content than what the compositor is seeing.
1396 scoped_refptr<media::VideoFrame> video_frame; 1035 scoped_refptr<media::VideoFrame> video_frame;
1397 base::WaitableEvent event(false, false); 1036 base::WaitableEvent event(false, false);
1398 compositor_task_runner_->PostTask(FROM_HERE, 1037 compositor_task_runner_->PostTask(FROM_HERE,
1399 base::Bind(&GetCurrentFrameAndSignal, 1038 base::Bind(&GetCurrentFrameAndSignal,
1400 base::Unretained(compositor_), 1039 base::Unretained(compositor_),
1401 &video_frame, 1040 &video_frame,
1402 &event)); 1041 &event));
1403 event.Wait(); 1042 event.Wait();
1404 return video_frame; 1043 return video_frame;
1405 } 1044 }
1406 1045
1407 } // namespace content 1046 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698