Chromium Code Reviews| Index: content/common/gpu/media/vt_video_encode_accelerator.cc | 
| diff --git a/content/common/gpu/media/vt_video_encode_accelerator.cc b/content/common/gpu/media/vt_video_encode_accelerator.cc | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..07b09c8dd5563a3a60d902c05aa923df497c79d3 | 
| --- /dev/null | 
| +++ b/content/common/gpu/media/vt_video_encode_accelerator.cc | 
| @@ -0,0 +1,312 @@ | 
| +// Copyright 2016 The Chromium Authors. All rights reserved. | 
| +// Use of this source code is governed by a BSD-style license that can be | 
| +// found in the LICENSE file. | 
| + | 
| +#include "content/common/gpu/media/vt_video_encode_accelerator.h" | 
| + | 
| +#include "base/thread_task_runner_handle.h" | 
| +#include "media/base/mac/coremedia_glue.h" | 
| +#include "media/base/mac/corevideo_glue.h" | 
| +#include "media/base/mac/video_frame_mac.h" | 
| +#include "media/base/mac/videotoolbox_helpers.h" | 
| + | 
| +namespace content { | 
| + | 
| +namespace { | 
| + | 
| +// Subjectively chosen. | 
| +const size_t kNumInputBuffers = 4; | 
| +const size_t kMaxFrameRateNumerator = 30; | 
| +const size_t kMaxFrameRateDenominator = 1; | 
| +const size_t kMaxResolutionWidth = 4096; | 
| +const size_t kMaxResolutionHeight = 2160; | 
| +const size_t kOutputBufferSizeRatio = 10; | 
| + | 
| +// Container for the associated data of a video frame being processed. | 
| +struct InProgressFrameEncode { | 
| + const base::TimeDelta timestamp; | 
| + const base::TimeTicks reference_time; | 
| + | 
| + InProgressFrameEncode( | 
| + base::TimeDelta rtp_timestamp, | 
| + base::TimeTicks ref_time) | 
| + : timestamp(rtp_timestamp), | 
| + reference_time(ref_time) {} | 
| +}; | 
| 
 
mcasas
2016/02/03 20:01:31
Recommend a 
private:
 DISALLOW_IMPLICIT_CONSTRUCT
 
emircan
2016/02/04 05:18:52
Done.
 
 | 
| + | 
| +} // namespace | 
| + | 
| +struct VTVideoEncodeAccelerator::BitstreamBufferRef { | 
| + BitstreamBufferRef(int32_t id, | 
| + scoped_ptr<base::SharedMemory> shm, | 
| + size_t size) | 
| + : id(id), shm(std::move(shm)), size(size) {} | 
| + const int32_t id; | 
| + const scoped_ptr<base::SharedMemory> shm; | 
| + const size_t size; | 
| +}; | 
| 
 
mcasas
2016/02/03 20:01:31
Same here, use DISALLOW_IMPLICIT_CONSTRUCTORS
 
emircan
2016/02/04 05:18:51
Done.
 
 | 
| + | 
| +VTVideoEncodeAccelerator::VTVideoEncodeAccelerator() | 
| + : videotoolbox_glue_(VideoToolboxGlue::Get()), | 
| + child_task_runner_(base::ThreadTaskRunnerHandle::Get()) { | 
| +} | 
| + | 
| +VTVideoEncodeAccelerator::~VTVideoEncodeAccelerator() { | 
| + DCHECK(child_task_runner_->BelongsToCurrentThread()); | 
| +} | 
| + | 
| +media::VideoEncodeAccelerator::SupportedProfiles | 
| +VTVideoEncodeAccelerator::GetSupportedProfiles() { | 
| + DVLOG(3) << __FUNCTION__; | 
| + | 
| + SupportedProfiles profiles; | 
| + SupportedProfile profile; | 
| + profile.profile = media::H264PROFILE_BASELINE; | 
| + profile.max_framerate_numerator = kMaxFrameRateNumerator; | 
| + profile.max_framerate_denominator = kMaxFrameRateDenominator; | 
| + profile.max_resolution = gfx::Size(kMaxResolutionWidth, kMaxResolutionHeight); | 
| + profiles.push_back(profile); | 
| + return profiles; | 
| +} | 
| + | 
| +bool VTVideoEncodeAccelerator::Initialize( | 
| + media::VideoPixelFormat format, | 
| + const gfx::Size& input_visible_size, | 
| + media::VideoCodecProfile output_profile, | 
| + uint32_t initial_bitrate, | 
| + Client* client) { | 
| + DVLOG(3) << __FUNCTION__ | 
| + << ": input_format=" << media::VideoPixelFormatToString(format) | 
| + << ", input_visible_size=" << input_visible_size.ToString() | 
| + << ", output_profile=" << output_profile | 
| + << ", initial_bitrate=" << initial_bitrate; | 
| + DCHECK(client); | 
| + DCHECK_EQ(media::PIXEL_FORMAT_I420, format); | 
| + DCHECK_EQ(media::H264PROFILE_BASELINE, output_profile); | 
| + | 
| + bitrate_ = initial_bitrate; | 
| + input_visible_size_ = input_visible_size; | 
| + | 
| + const bool session_created = ResetCompressionSession(); | 
| 
 
mcasas
2016/02/03 20:01:31
No need for temporary?
 
emircan
2016/02/04 05:18:52
Done. I was trying to be verbose.
 
 | 
| + if (!session_created) { | 
| + DLOG(ERROR) << "Failed creating compression session"; | 
| + return false; | 
| + } | 
| + | 
| + client_ptr_factory_.reset(new base::WeakPtrFactory<Client>(client)); | 
| 
 
mcasas
2016/02/03 20:01:31
This |client_ptr_factory_| is only used here,
do y
 
emircan
2016/02/04 05:18:51
I can invalidate the weakptrs in Destroy() using i
 
 | 
| + client_ = client_ptr_factory_->GetWeakPtr(); | 
| + | 
| + child_task_runner_->PostTask( | 
| + FROM_HERE, | 
| + base::Bind(&Client::RequireBitstreamBuffers, client_, kNumInputBuffers, | 
| + input_visible_size_, | 
| + input_visible_size_.GetArea() / kOutputBufferSizeRatio)); | 
| 
 
hbos_chromium
2016/02/03 18:21:08
About thread-safety, how is this class used across
 
emircan
2016/02/04 05:18:52
Thanks for pointing it out. Actually, I don't need
 
 | 
| + return true; | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::Encode( | 
| + const scoped_refptr<media::VideoFrame>& frame, | 
| + bool force_keyframe) { | 
| + DVLOG(3) << __FUNCTION__; | 
| + DCHECK(thread_checker_.CalledOnValidThread()); | 
| + DCHECK(compression_session_); | 
| + DCHECK(frame); | 
| + | 
| + base::TimeTicks ref_time; | 
| + if (!frame->metadata()->GetTimeTicks( | 
| + media::VideoFrameMetadata::REFERENCE_TIME, &ref_time)) { | 
| + ref_time = base::TimeTicks::Now(); | 
| + } | 
| + auto timestamp_cm = CoreMediaGlue::CMTimeMake( | 
| + (ref_time - base::TimeTicks()).InMicroseconds(), USEC_PER_SEC); | 
| + // Wrap information we'll need after the frame is encoded in a heap object. | 
| + // We'll get the pointer back from the VideoToolbox completion callback. | 
| + scoped_ptr<InProgressFrameEncode> request(new InProgressFrameEncode( | 
| + frame->timestamp(), ref_time)); | 
| + | 
| + // TODO(emircan): See if we can eliminate a copy here by using | 
| + // CVPixelBufferPool for the allocation of incoming video frames. | 
| 
 
mcasas
2016/02/03 20:01:31
nit: s/video frames/VideoFrames/?
 
emircan
2016/02/04 05:18:52
Done.
 
 | 
| + base::ScopedCFTypeRef<CVPixelBufferRef> pixel_buffer = | 
| + media::WrapVideoFrameInCVPixelBuffer(*frame); | 
| + base::ScopedCFTypeRef<CFDictionaryRef> frame_props = | 
| + media::DictionaryWithKeyValue( | 
| + videotoolbox_glue_->kVTEncodeFrameOptionKey_ForceKeyFrame(), | 
| + force_keyframe ? kCFBooleanTrue : kCFBooleanFalse); | 
| + | 
| + OSStatus status = videotoolbox_glue_->VTCompressionSessionEncodeFrame( | 
| + compression_session_, pixel_buffer, timestamp_cm, | 
| + CoreMediaGlue::CMTime{0, 0, 0, 0}, frame_props, | 
| + reinterpret_cast<void*>(request.release()), nullptr); | 
| + DLOG_IF(ERROR, status != noErr) | 
| + << " VTCompressionSessionEncodeFrame failed: " << status; | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::UseOutputBitstreamBuffer( | 
| + const media::BitstreamBuffer& buffer) { | 
| + DVLOG(3) << __FUNCTION__; | 
| + DCHECK(child_task_runner_->BelongsToCurrentThread()); | 
| + DCHECK_GE(buffer.size(), static_cast<size_t>(input_visible_size_.GetArea() / | 
| + kOutputBufferSizeRatio)); | 
| + | 
| + scoped_ptr<base::SharedMemory> shm( | 
| + new base::SharedMemory(buffer.handle(), false)); | 
| + if (!shm->Map(buffer.size())) { | 
| + DLOG(ERROR) << "Failed mapping shared memory."; | 
| + return; | 
| + } | 
| + | 
| + scoped_ptr<BitstreamBufferRef> buffer_ref( | 
| + new BitstreamBufferRef(buffer.id(), std::move(shm), buffer.size())); | 
| + encoder_output_queue_.push_back(std::move(buffer_ref)); | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::RequestEncodingParametersChange( | 
| + uint32_t bitrate, | 
| + uint32_t framerate) { | 
| + DVLOG(3) << __FUNCTION__; | 
| + | 
| + bitrate_ = bitrate; | 
| + if (compression_session_) { | 
| 
 
hbos_chromium
2016/02/03 18:21:08
Does it make sense to DCHECK instead?
 
mcasas
2016/02/03 20:01:31
nit: 
if (!compression_session_)
  return;
...
 
emircan
2016/02/04 05:18:52
Done.
 
emircan
2016/02/04 05:18:52
Acknowledged.
 
 | 
| + media::SetSessionProperty( | 
| + compression_session_, videotoolbox_glue_, | 
| + videotoolbox_glue_->kVTCompressionPropertyKey_AverageBitRate(), | 
| + static_cast<int32_t>(bitrate_)); | 
| + } | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::Destroy() { | 
| + DVLOG(3) << __FUNCTION__; | 
| + | 
| + client_ptr_factory_.reset(); | 
| + DestroyCompressionSession(); | 
| + delete this; | 
| +} | 
| + | 
| +// static | 
| +void VTVideoEncodeAccelerator::CompressionCallback(void* encoder_opaque, | 
| + void* request_opaque, | 
| + OSStatus status, | 
| + VTEncodeInfoFlags info, | 
| + CMSampleBufferRef sbuf) { | 
| + DVLOG(3) << __FUNCTION__; | 
| + DLOG_IF(ERROR, status != noErr) << " encode failed: " << status; | 
| + | 
| + if (info & VideoToolboxGlue::kVTEncodeInfo_FrameDropped) { | 
| + DVLOG(2) << " frame dropped"; | 
| + return; | 
| + } | 
| + | 
| + auto sample_attachments = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex( | 
| + CoreMediaGlue::CMSampleBufferGetSampleAttachmentsArray(sbuf, true), 0)); | 
| + const bool keyframe = | 
| + !CFDictionaryContainsKey(sample_attachments, | 
| + CoreMediaGlue::kCMSampleAttachmentKey_NotSync()); | 
| + | 
| + auto encoder = reinterpret_cast<VTVideoEncodeAccelerator*>(encoder_opaque); | 
| + if (encoder->encoder_output_queue_.empty()) { | 
| + DLOG(ERROR) << "No more bitstream buffer to encode into."; | 
| + return; | 
| + } | 
| + scoped_ptr<VTVideoEncodeAccelerator::BitstreamBufferRef> buffer_ref = | 
| + std::move(encoder->encoder_output_queue_.front()); | 
| + encoder->encoder_output_queue_.pop_front(); | 
| + | 
| + std::string annexb_buffer; | 
| + media::CopySampleBufferToAnnexBBuffer(sbuf, &annexb_buffer, keyframe); | 
| + if (buffer_ref->size < annexb_buffer.size()) { | 
| + DLOG(ERROR) << "Cannot fit the encode output into bitstream buffer."; | 
| + return; | 
| + } | 
| + memcpy(buffer_ref->shm->memory(), annexb_buffer.data(), | 
| + annexb_buffer.size()); | 
| + | 
| + encoder->child_task_runner_->PostTask( | 
| + FROM_HERE, base::Bind(&Client::BitstreamBufferReady, encoder->client_, | 
| + buffer_ref->id, annexb_buffer.size(), keyframe)); | 
| +} | 
| + | 
| +bool VTVideoEncodeAccelerator::ResetCompressionSession() { | 
| + DCHECK(thread_checker_.CalledOnValidThread()); | 
| + | 
| + DestroyCompressionSession(); | 
| + | 
| + // TODO(emircan): Investigate IOS constraints. | 
| 
 
mcasas
2016/02/03 20:01:31
?
 
emircan
2016/02/04 05:18:51
Removed.
 
 | 
| + base::ScopedCFTypeRef<CFDictionaryRef> encoder_spec; | 
| + encoder_spec = media::DictionaryWithKeyValue( | 
| + videotoolbox_glue_ | 
| + ->kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder(), | 
| + kCFBooleanTrue); | 
| + | 
| + // Keep these in-sync with those in ConfigureSession(). | 
| + CFTypeRef attributes_keys[] = { | 
| +#if defined(OS_IOS) | 
| + kCVPixelBufferOpenGLESCompatibilityKey, | 
| +#else | 
| + kCVPixelBufferOpenGLCompatibilityKey, | 
| +#endif | 
| + kCVPixelBufferIOSurfacePropertiesKey, | 
| + kCVPixelBufferPixelFormatTypeKey | 
| + }; | 
| + const int format[] = { | 
| + CoreVideoGlue::kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange}; | 
| + CFTypeRef attributes_values[] = { | 
| + kCFBooleanTrue, | 
| + media::DictionaryWithKeysAndValues(nullptr, nullptr, 0).release(), | 
| + media::ArrayWithIntegers(format, arraysize(format)).release()}; | 
| + const base::ScopedCFTypeRef<CFDictionaryRef> attributes = | 
| + media::DictionaryWithKeysAndValues(attributes_keys, | 
| + attributes_values, | 
| + arraysize(attributes_keys)); | 
| + for (auto& v : attributes_values) | 
| + CFRelease(v); | 
| + | 
| + // Create the compression session. | 
| + OSStatus status = videotoolbox_glue_->VTCompressionSessionCreate( | 
| + kCFAllocatorDefault, | 
| + input_visible_size_.width(), | 
| + input_visible_size_.height(), | 
| + CoreMediaGlue::kCMVideoCodecType_H264, | 
| + encoder_spec, | 
| + attributes, | 
| + nullptr /* compressedDataAllocator */, | 
| + &VTVideoEncodeAccelerator::CompressionCallback, | 
| + reinterpret_cast<void*>(this), | 
| + compression_session_.InitializeInto()); | 
| + if (status != noErr) { | 
| + DLOG(ERROR) << " VTCompressionSessionCreate failed: " << status; | 
| + return false; | 
| + } | 
| + ConfigureCompressionSession(); | 
| + return true; | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::ConfigureCompressionSession() { | 
| + DCHECK(compression_session_); | 
| + | 
| + media::SetSessionProperty( | 
| + compression_session_, videotoolbox_glue_, | 
| + videotoolbox_glue_->kVTCompressionPropertyKey_ProfileLevel(), | 
| + videotoolbox_glue_->kVTProfileLevel_H264_Baseline_AutoLevel()); | 
| + media::SetSessionProperty( | 
| + compression_session_, videotoolbox_glue_, | 
| + videotoolbox_glue_->kVTCompressionPropertyKey_RealTime(), true); | 
| + media::SetSessionProperty( | 
| + compression_session_, videotoolbox_glue_, | 
| + videotoolbox_glue_->kVTCompressionPropertyKey_AverageBitRate(), | 
| + static_cast<int32_t>(bitrate_)); | 
| + media::SetSessionProperty( | 
| + compression_session_, videotoolbox_glue_, | 
| + videotoolbox_glue_->kVTCompressionPropertyKey_AllowFrameReordering(), | 
| + false); | 
| +} | 
| + | 
| +void VTVideoEncodeAccelerator::DestroyCompressionSession() { | 
| + DCHECK(thread_checker_.CalledOnValidThread()); | 
| + | 
| + if (compression_session_) { | 
| + videotoolbox_glue_->VTCompressionSessionInvalidate(compression_session_); | 
| + compression_session_.reset(); | 
| + } | 
| +} | 
| + | 
| +} // namespace content |