Commit 39d51310 authored by CJ DiMeglio's avatar CJ DiMeglio Committed by Commit Bot

Gives WebMediaPlayerMSCompositor access to VideoFrameSubmitter.

This CL makes it so that WebMediaPlayerMSCompositor can make proper calls
to a VideoFrameSubmitter to use cc::Surfaces for video instead of the
video layer. With this CL, cc::Surface for Video now works end to end
with WebMediaPlayerMS.

Bug: 746182
Change-Id: I90e100ba06dc7570d6ab10d6dc3862681fe80552
Reviewed-on: https://chromium-review.googlesource.com/1090031
Commit-Queue: CJ DiMeglio <lethalantidote@chromium.org>
Reviewed-by: default avatarEmircan Uysaler <emircan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#591163}
parent 9bdb9118
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include "base/task_runner_util.h" #include "base/task_runner_util.h"
#include "base/threading/thread_task_runner_handle.h" #include "base/threading/thread_task_runner_handle.h"
#include "build/buildflag.h" #include "build/buildflag.h"
#include "cc/trees/layer_tree_settings.h"
#include "content/public/common/content_client.h" #include "content/public/common/content_client.h"
#include "content/public/renderer/content_renderer_client.h" #include "content/public/renderer/content_renderer_client.h"
#include "content/renderer/media/audio/audio_device_factory.h" #include "content/renderer/media/audio/audio_device_factory.h"
...@@ -216,8 +217,8 @@ blink::WebMediaPlayer* MediaFactory::CreateMediaPlayer( ...@@ -216,8 +217,8 @@ blink::WebMediaPlayer* MediaFactory::CreateMediaPlayer(
blink::WebMediaStream web_stream = blink::WebMediaStream web_stream =
GetWebMediaStreamFromWebMediaPlayerSource(source); GetWebMediaStreamFromWebMediaPlayerSource(source);
if (!web_stream.IsNull()) if (!web_stream.IsNull())
return CreateWebMediaPlayerForMediaStream(client, sink_id, security_origin, return CreateWebMediaPlayerForMediaStream(
web_frame, layer_tree_view); client, sink_id, security_origin, web_frame, layer_tree_view, settings);
// If |source| was not a MediaStream, it must be a URL. // If |source| was not a MediaStream, it must be a URL.
// TODO(guidou): Fix this when support for other srcObject types is added. // TODO(guidou): Fix this when support for other srcObject types is added.
...@@ -500,7 +501,8 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream( ...@@ -500,7 +501,8 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream(
const blink::WebString& sink_id, const blink::WebString& sink_id,
const blink::WebSecurityOrigin& security_origin, const blink::WebSecurityOrigin& security_origin,
blink::WebLocalFrame* frame, blink::WebLocalFrame* frame,
blink::WebLayerTreeView* layer_tree_view) { blink::WebLayerTreeView* layer_tree_view,
const cc::LayerTreeSettings& settings) {
RenderThreadImpl* const render_thread = RenderThreadImpl::current(); RenderThreadImpl* const render_thread = RenderThreadImpl::current();
scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner = scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner =
...@@ -509,6 +511,12 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream( ...@@ -509,6 +511,12 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream(
compositor_task_runner = compositor_task_runner =
render_frame_->GetTaskRunner(blink::TaskType::kInternalMediaRealTime); render_frame_->GetTaskRunner(blink::TaskType::kInternalMediaRealTime);
scoped_refptr<base::SingleThreadTaskRunner>
media_stream_compositor_task_runner =
VideoSurfaceLayerEnabledForMS()
? render_thread->CreateVideoFrameCompositorTaskRunner()
: compositor_task_runner;
DCHECK(layer_tree_view); DCHECK(layer_tree_view);
return new WebMediaPlayerMS( return new WebMediaPlayerMS(
frame, client, GetWebMediaPlayerDelegate(), frame, client, GetWebMediaPlayerDelegate(),
...@@ -516,10 +524,17 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream( ...@@ -516,10 +524,17 @@ blink::WebMediaPlayer* MediaFactory::CreateWebMediaPlayerForMediaStream(
url::Origin(security_origin).GetURL(), url::Origin(security_origin).GetURL(),
render_frame_->GetTaskRunner(blink::TaskType::kInternalMedia)), render_frame_->GetTaskRunner(blink::TaskType::kInternalMedia)),
CreateMediaStreamRendererFactory(), render_thread->GetIOTaskRunner(), CreateMediaStreamRendererFactory(), render_thread->GetIOTaskRunner(),
compositor_task_runner, render_thread->GetMediaThreadTaskRunner(), media_stream_compositor_task_runner,
render_thread->GetMediaThreadTaskRunner(),
render_thread->GetWorkerTaskRunner(), render_thread->GetGpuFactories(), render_thread->GetWorkerTaskRunner(), render_thread->GetGpuFactories(),
sink_id, sink_id,
base::BindOnce(&blink::WebSurfaceLayerBridge::Create, layer_tree_view), base::BindOnce(&blink::WebSurfaceLayerBridge::Create, layer_tree_view),
base::BindRepeating(
&blink::WebVideoFrameSubmitter::Create,
base::BindRepeating(
&PostContextProviderToCallback,
RenderThreadImpl::current()->GetCompositorMainThreadTaskRunner()),
settings),
VideoSurfaceLayerEnabledForMS()); VideoSurfaceLayerEnabledForMS());
} }
......
...@@ -127,7 +127,8 @@ class MediaFactory { ...@@ -127,7 +127,8 @@ class MediaFactory {
const blink::WebString& sink_id, const blink::WebString& sink_id,
const blink::WebSecurityOrigin& security_origin, const blink::WebSecurityOrigin& security_origin,
blink::WebLocalFrame* frame, blink::WebLocalFrame* frame,
blink::WebLayerTreeView* layer_tree_view); blink::WebLayerTreeView* layer_tree_view,
const cc::LayerTreeSettings& settings);
// Returns the media delegate for WebMediaPlayer usage. If // Returns the media delegate for WebMediaPlayer usage. If
// |media_player_delegate_| is NULL, one is created. // |media_player_delegate_| is NULL, one is created.
......
...@@ -269,6 +269,8 @@ WebMediaPlayerMS::WebMediaPlayerMS( ...@@ -269,6 +269,8 @@ WebMediaPlayerMS::WebMediaPlayerMS(
media::GpuVideoAcceleratorFactories* gpu_factories, media::GpuVideoAcceleratorFactories* gpu_factories,
const blink::WebString& sink_id, const blink::WebString& sink_id,
CreateSurfaceLayerBridgeCB create_bridge_callback, CreateSurfaceLayerBridgeCB create_bridge_callback,
base::RepeatingCallback<std::unique_ptr<blink::WebVideoFrameSubmitter>()>
create_submitter_callback,
bool surface_layer_for_video_enabled) bool surface_layer_for_video_enabled)
: frame_(frame), : frame_(frame),
network_state_(WebMediaPlayer::kNetworkStateEmpty), network_state_(WebMediaPlayer::kNetworkStateEmpty),
...@@ -291,6 +293,7 @@ WebMediaPlayerMS::WebMediaPlayerMS( ...@@ -291,6 +293,7 @@ WebMediaPlayerMS::WebMediaPlayerMS(
volume_multiplier_(1.0), volume_multiplier_(1.0),
should_play_upon_shown_(false), should_play_upon_shown_(false),
create_bridge_callback_(std::move(create_bridge_callback)), create_bridge_callback_(std::move(create_bridge_callback)),
create_submitter_callback_(create_submitter_callback),
surface_layer_for_video_enabled_(surface_layer_for_video_enabled) { surface_layer_for_video_enabled_(surface_layer_for_video_enabled) {
DVLOG(1) << __func__; DVLOG(1) << __func__;
DCHECK(client); DCHECK(client);
...@@ -349,7 +352,9 @@ blink::WebMediaPlayer::LoadTiming WebMediaPlayerMS::Load( ...@@ -349,7 +352,9 @@ blink::WebMediaPlayer::LoadTiming WebMediaPlayerMS::Load(
web_stream_.AddObserver(this); web_stream_.AddObserver(this);
compositor_ = new WebMediaPlayerMSCompositor( compositor_ = new WebMediaPlayerMSCompositor(
compositor_task_runner_, io_task_runner_, web_stream_, AsWeakPtr()); compositor_task_runner_, io_task_runner_, web_stream_,
create_submitter_callback_, surface_layer_for_video_enabled_,
AsWeakPtr());
SetNetworkState(WebMediaPlayer::kNetworkStateLoading); SetNetworkState(WebMediaPlayer::kNetworkStateLoading);
SetReadyState(WebMediaPlayer::kReadyStateHaveNothing); SetReadyState(WebMediaPlayer::kReadyStateHaveNothing);
...@@ -1079,6 +1084,10 @@ bool WebMediaPlayerMS::TexImageImpl(TexImageFunctionID functionID, ...@@ -1079,6 +1084,10 @@ bool WebMediaPlayerMS::TexImageImpl(TexImageFunctionID functionID,
return false; return false;
} }
void WebMediaPlayerMS::OnFrameSinkDestroyed() {
bridge_->ClearSurfaceId();
}
void WebMediaPlayerMS::OnFirstFrameReceived(media::VideoRotation video_rotation, void WebMediaPlayerMS::OnFirstFrameReceived(media::VideoRotation video_rotation,
bool is_opaque) { bool is_opaque) {
DVLOG(1) << __func__; DVLOG(1) << __func__;
...@@ -1090,6 +1099,16 @@ void WebMediaPlayerMS::OnFirstFrameReceived(media::VideoRotation video_rotation, ...@@ -1090,6 +1099,16 @@ void WebMediaPlayerMS::OnFirstFrameReceived(media::VideoRotation video_rotation,
bridge_ = std::move(create_bridge_callback_) bridge_ = std::move(create_bridge_callback_)
.Run(this, compositor_->GetUpdateSubmissionStateCallback()); .Run(this, compositor_->GetUpdateSubmissionStateCallback());
bridge_->CreateSurfaceLayer(); bridge_->CreateSurfaceLayer();
bridge_->SetContentsOpaque(opaque_);
compositor_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&WebMediaPlayerMSCompositor::EnableSubmission, compositor_,
bridge_->GetSurfaceId(), video_rotation, IsInPictureInPicture(),
opaque_,
media::BindToCurrentLoop(base::BindRepeating(
&WebMediaPlayerMS::OnFrameSinkDestroyed, AsWeakPtr()))));
} }
SetReadyState(WebMediaPlayer::kReadyStateHaveMetadata); SetReadyState(WebMediaPlayer::kReadyStateHaveMetadata);
...@@ -1112,6 +1131,9 @@ void WebMediaPlayerMS::OnOpacityChanged(bool is_opaque) { ...@@ -1112,6 +1131,9 @@ void WebMediaPlayerMS::OnOpacityChanged(bool is_opaque) {
DCHECK(bridge_); DCHECK(bridge_);
bridge_->SetContentsOpaque(opaque_); bridge_->SetContentsOpaque(opaque_);
compositor_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&WebMediaPlayerMSCompositor::UpdateIsOpaque,
compositor_, opaque_));
} }
} }
...@@ -1132,12 +1154,18 @@ void WebMediaPlayerMS::OnRotationChanged(media::VideoRotation video_rotation, ...@@ -1132,12 +1154,18 @@ void WebMediaPlayerMS::OnRotationChanged(media::VideoRotation video_rotation,
get_client()->SetCcLayer(new_video_layer.get()); get_client()->SetCcLayer(new_video_layer.get());
video_layer_ = std::move(new_video_layer); video_layer_ = std::move(new_video_layer);
} else if (bridge_->GetCcLayer()) { } else {
// TODO(lethalantidote): Handle rotation. compositor_task_runner_->PostTask(
bridge_->SetContentsOpaque(opaque_); FROM_HERE, base::BindOnce(&WebMediaPlayerMSCompositor::UpdateRotation,
compositor_, video_rotation));
} }
} }
bool WebMediaPlayerMS::IsInPictureInPicture() const {
// TODO(apacible): Add implementation. See http://crbug/746182.
return false;
}
void WebMediaPlayerMS::RepaintInternal() { void WebMediaPlayerMS::RepaintInternal() {
DVLOG(1) << __func__; DVLOG(1) << __func__;
DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(thread_checker_.CalledOnValidThread());
......
...@@ -27,6 +27,7 @@ namespace blink { ...@@ -27,6 +27,7 @@ namespace blink {
class WebLocalFrame; class WebLocalFrame;
class WebMediaPlayerClient; class WebMediaPlayerClient;
class WebString; class WebString;
class WebVideoFrameSubmitter;
} }
namespace media { namespace media {
...@@ -91,6 +92,8 @@ class CONTENT_EXPORT WebMediaPlayerMS ...@@ -91,6 +92,8 @@ class CONTENT_EXPORT WebMediaPlayerMS
media::GpuVideoAcceleratorFactories* gpu_factories, media::GpuVideoAcceleratorFactories* gpu_factories,
const blink::WebString& sink_id, const blink::WebString& sink_id,
CreateSurfaceLayerBridgeCB create_bridge_callback, CreateSurfaceLayerBridgeCB create_bridge_callback,
base::RepeatingCallback<std::unique_ptr<blink::WebVideoFrameSubmitter>()>
create_submitter_callback,
bool surface_layer_for_video_enabled_); bool surface_layer_for_video_enabled_);
~WebMediaPlayerMS() override; ~WebMediaPlayerMS() override;
...@@ -237,11 +240,18 @@ class CONTENT_EXPORT WebMediaPlayerMS ...@@ -237,11 +240,18 @@ class CONTENT_EXPORT WebMediaPlayerMS
static const gfx::Size kUseGpuMemoryBufferVideoFramesMinResolution; static const gfx::Size kUseGpuMemoryBufferVideoFramesMinResolution;
#endif // defined(OS_WIN) #endif // defined(OS_WIN)
// When we lose the context_provider, we destroy the CompositorFrameSink to
// prevent frames from being submitted. The current surface_ids become
// invalid.
void OnFrameSinkDestroyed();
void OnFirstFrameReceived(media::VideoRotation video_rotation, void OnFirstFrameReceived(media::VideoRotation video_rotation,
bool is_opaque); bool is_opaque);
void OnOpacityChanged(bool is_opaque); void OnOpacityChanged(bool is_opaque);
void OnRotationChanged(media::VideoRotation video_rotation, bool is_opaque); void OnRotationChanged(media::VideoRotation video_rotation, bool is_opaque);
bool IsInPictureInPicture() const;
// Need repaint due to state change. // Need repaint due to state change.
void RepaintInternal(); void RepaintInternal();
...@@ -309,6 +319,7 @@ class CONTENT_EXPORT WebMediaPlayerMS ...@@ -309,6 +319,7 @@ class CONTENT_EXPORT WebMediaPlayerMS
const scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_; const scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_;
const scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner_; const scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner_;
const scoped_refptr<base::SingleThreadTaskRunner> media_task_runner_; const scoped_refptr<base::SingleThreadTaskRunner> media_task_runner_;
const scoped_refptr<base::TaskRunner> worker_task_runner_; const scoped_refptr<base::TaskRunner> worker_task_runner_;
media::GpuVideoAcceleratorFactories* gpu_factories_; media::GpuVideoAcceleratorFactories* gpu_factories_;
...@@ -336,6 +347,9 @@ class CONTENT_EXPORT WebMediaPlayerMS ...@@ -336,6 +347,9 @@ class CONTENT_EXPORT WebMediaPlayerMS
CreateSurfaceLayerBridgeCB create_bridge_callback_; CreateSurfaceLayerBridgeCB create_bridge_callback_;
base::RepeatingCallback<std::unique_ptr<blink::WebVideoFrameSubmitter>()>
create_submitter_callback_;
// Whether the use of a surface layer instead of a video layer is enabled. // Whether the use of a surface layer instead of a video layer is enabled.
bool surface_layer_for_video_enabled_ = false; bool surface_layer_for_video_enabled_ = false;
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h" #include "base/message_loop/message_loop.h"
#include "base/synchronization/lock.h" #include "base/synchronization/lock.h"
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include "cc/layers/video_frame_provider.h" #include "cc/layers/video_frame_provider.h"
#include "content/common/content_export.h" #include "content/common/content_export.h"
#include "media/base/media_log.h" #include "media/base/media_log.h"
#include "third_party/blink/public/platform/web_video_frame_submitter.h"
namespace base { namespace base {
class SingleThreadTaskRunner; class SingleThreadTaskRunner;
...@@ -37,6 +38,10 @@ namespace media { ...@@ -37,6 +38,10 @@ namespace media {
class VideoRendererAlgorithm; class VideoRendererAlgorithm;
} }
namespace viz {
class SurfaceId;
}
namespace content { namespace content {
class WebMediaPlayerMS; class WebMediaPlayerMS;
...@@ -51,16 +56,19 @@ class WebMediaPlayerMS; ...@@ -51,16 +56,19 @@ class WebMediaPlayerMS;
// frame, and submit it whenever asked by the compositor. // frame, and submit it whenever asked by the compositor.
class CONTENT_EXPORT WebMediaPlayerMSCompositor class CONTENT_EXPORT WebMediaPlayerMSCompositor
: public cc::VideoFrameProvider, : public cc::VideoFrameProvider,
public base::RefCountedThreadSafe<WebMediaPlayerMSCompositor> { public base::RefCountedDeleteOnSequence<WebMediaPlayerMSCompositor> {
public: public:
// This |url| represents the media stream we are rendering. |url| is used to // This |url| represents the media stream we are rendering. |url| is used to
// find out what web stream this WebMediaPlayerMSCompositor is playing, and // find out what web stream this WebMediaPlayerMSCompositor is playing, and
// together with flag "--disable-rtc-smoothness-algorithm" determine whether // together with flag "--disable-rtc-smoothness-algorithm" determine whether
// we enable algorithm or not. // we enable algorithm or not.
WebMediaPlayerMSCompositor( WebMediaPlayerMSCompositor(
scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner, scoped_refptr<base::SingleThreadTaskRunner> task_runner,
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner, scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
const blink::WebMediaStream& web_stream, const blink::WebMediaStream& web_stream,
base::RepeatingCallback<std::unique_ptr<blink::WebVideoFrameSubmitter>()>
create_submitter_callback,
bool surface_layer_for_video_enabled,
const base::WeakPtr<WebMediaPlayerMS>& player); const base::WeakPtr<WebMediaPlayerMS>& player);
// Can be called from any thread. // Can be called from any thread.
...@@ -76,6 +84,24 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor ...@@ -76,6 +84,24 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor
size_t total_frame_count(); size_t total_frame_count();
size_t dropped_frame_count(); size_t dropped_frame_count();
// Signals the VideoFrameSubmitter to prepare to receive BeginFrames and
// submit video frames given by WebMediaPlayerMSCompositor.
virtual void EnableSubmission(
const viz::SurfaceId& id,
media::VideoRotation rotation,
bool force_submit,
bool is_opaque,
blink::WebFrameSinkDestroyedCallback frame_sink_destroyed_callback);
// Updates the rotation information for frames given to |submitter_|.
void UpdateRotation(media::VideoRotation rotation);
// Notifies the |submitter_| that the frames must be submitted.
void SetForceSubmit(bool);
// Updates the opacity information for frames given to |submitter_|.
void UpdateIsOpaque(bool);
// VideoFrameProvider implementation. // VideoFrameProvider implementation.
void SetVideoFrameProviderClient( void SetVideoFrameProviderClient(
cc::VideoFrameProvider::Client* client) override; cc::VideoFrameProvider::Client* client) override;
...@@ -101,11 +127,19 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor ...@@ -101,11 +127,19 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor
void StopUsingProvider(); void StopUsingProvider();
private: private:
friend class base::RefCountedThreadSafe<WebMediaPlayerMSCompositor>; friend class base::RefCountedDeleteOnSequence<WebMediaPlayerMSCompositor>;
friend class base::DeleteHelper<WebMediaPlayerMSCompositor>;
friend class WebMediaPlayerMSTest; friend class WebMediaPlayerMSTest;
~WebMediaPlayerMSCompositor() override; ~WebMediaPlayerMSCompositor() override;
// Ran on the |video_frame_compositor_task_runner_| to initialize
// |submitter_|
void InitializeSubmitter();
// Signals the VideoFrameSubmitter to stop submitting frames.
void UpdateSubmissionState(bool);
bool MapTimestampsToRenderTimeTicks( bool MapTimestampsToRenderTimeTicks(
const std::vector<base::TimeDelta>& timestamps, const std::vector<base::TimeDelta>& timestamps,
std::vector<base::TimeTicks>* wall_clock_times); std::vector<base::TimeTicks>* wall_clock_times);
...@@ -138,7 +172,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor ...@@ -138,7 +172,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor
// which is renderer main thread in this class. // which is renderer main thread in this class.
base::ThreadChecker thread_checker_; base::ThreadChecker thread_checker_;
const scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner_; const scoped_refptr<base::SingleThreadTaskRunner>
video_frame_compositor_task_runner_;
const scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_; const scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_;
base::MessageLoop* main_message_loop_; base::MessageLoop* main_message_loop_;
...@@ -186,6 +221,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor ...@@ -186,6 +221,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor
bool stopped_; bool stopped_;
bool render_started_; bool render_started_;
std::unique_ptr<blink::WebVideoFrameSubmitter> submitter_;
std::map<base::TimeDelta, base::TimeTicks> timestamps_to_clock_times_; std::map<base::TimeDelta, base::TimeTicks> timestamps_to_clock_times_;
cc::UpdateSubmissionStateCB update_submission_state_callback_; cc::UpdateSubmissionStateCB update_submission_state_callback_;
...@@ -194,6 +231,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor ...@@ -194,6 +231,8 @@ class CONTENT_EXPORT WebMediaPlayerMSCompositor
// |dropped_frame_count_|, and |render_started_|. // |dropped_frame_count_|, and |render_started_|.
base::Lock current_frame_lock_; base::Lock current_frame_lock_;
base::WeakPtrFactory<WebMediaPlayerMSCompositor> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(WebMediaPlayerMSCompositor); DISALLOW_COPY_AND_ASSIGN(WebMediaPlayerMSCompositor);
}; };
} // namespace content } // namespace content
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment