// Copyright 2017 Google Inc. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
#include "starboard/shared/win32/win32_video_decoder.h" | |
#include <Codecapi.h> | |
#include <D3d11_1.h> | |
#include <queue> | |
#include <utility> | |
#include "starboard/shared/starboard/thread_checker.h" | |
#include "starboard/shared/win32/atomic_queue.h" | |
#include "starboard/shared/win32/decrypting_decoder.h" | |
#include "starboard/shared/win32/dx_context_video_decoder.h" | |
#include "starboard/shared/win32/error_utils.h" | |
#include "starboard/shared/win32/media_common.h" | |
#include "starboard/shared/win32/media_foundation_utils.h" | |
#include "starboard/shared/win32/video_texture.h" | |
#include "starboard/shared/win32/video_transform.h" | |
#include "starboard/window.h" | |
namespace starboard { | |
namespace shared { | |
namespace win32 { | |
using Microsoft::WRL::ComPtr; | |
using ::starboard::shared::starboard::ThreadChecker; | |
using ::starboard::shared::win32::CheckResult; | |
namespace { | |
// In MS sample it is set to 5 and we experienced issues when this is set to | |
// below 5. This can be further tuned to balance between playback smoothness | |
// and memory consumption. | |
const int kSampleAllocatorFramesMax = 12; | |
// This is the minimum allocated frames in the output ring buffer. Can be | |
// further tuned to save memory. Note that use a value that is too small leads | |
// to hang when calling ProcessOutput(). | |
const int kMinimumOutputSampleCount = kSampleAllocatorFramesMax + 5; | |
// CLSID_CMSVideoDecoderMFT {62CE7E72-4C71-4D20-B15D-452831A87D9D} | |
const GUID CLSID_VideoDecoder = {0x62CE7E72, 0x4C71, 0x4d20, 0xB1, 0x5D, 0x45, | |
0x28, 0x31, 0xA8, 0x7D, 0x9D}; | |
class AbstractWin32VideoDecoderImpl : public AbstractWin32VideoDecoder { | |
public: | |
AbstractWin32VideoDecoderImpl(SbMediaVideoCodec codec, SbDrmSystem drm_system) | |
: thread_checker_(ThreadChecker::kSetThreadIdOnFirstCheck), | |
codec_(codec) { | |
SbMemorySet(&display_aperture_, 0, sizeof(RECT)); | |
Configure(drm_system); | |
} | |
void Consume(ComPtr<IMFSample> sample) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
VideoFramePtr frame_output = | |
VideoFrameFactory::Construct(sample, display_aperture_, | |
video_blt_interfaces_); | |
output_queue_.push(frame_output); | |
} | |
void OnNewOutputType(const ComPtr<IMFMediaType>& type) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
RECT rect = {}; | |
MFVideoArea aperture; | |
HRESULT hr = type->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, | |
reinterpret_cast<UINT8*>(&aperture), sizeof(MFVideoArea), nullptr); | |
if (SUCCEEDED(hr)) { | |
display_aperture_.left = aperture.OffsetX.value; | |
display_aperture_.right = rect.left + aperture.Area.cx; | |
display_aperture_.top = aperture.OffsetY.value; | |
display_aperture_.bottom = rect.top + aperture.Area.cy; | |
return; | |
} | |
uint32_t width; | |
uint32_t height; | |
hr = MFGetAttributeSize(type.Get(), MF_MT_FRAME_SIZE, | |
&width, &height); | |
if (SUCCEEDED(hr)) { | |
display_aperture_.left = 0; | |
display_aperture_.top = 0; | |
display_aperture_.right = rect.left + width; | |
display_aperture_.bottom = rect.top + height; | |
} | |
} | |
void Configure(SbDrmSystem drm_system) { | |
scoped_ptr<MediaTransform> media_transform; | |
// If this is updated then media_is_video_supported.cc also needs to be | |
// updated. | |
switch (codec_) { | |
case kSbMediaVideoCodecH264: { | |
media_transform = CreateH264Transform(kVideoFormat_YV12); | |
break; | |
} | |
case kSbMediaVideoCodecVp9: { | |
// VP9 decoder needs a default resolution. | |
media_transform = TryCreateVP9Transform(kVideoFormat_YV12, 1024, 768); | |
break; | |
} | |
default: { SB_NOTREACHED(); } | |
} | |
impl_.reset( | |
new DecryptingDecoder("video", media_transform.Pass(), drm_system)); | |
MediaTransform* decoder = impl_->GetDecoder(); | |
dx_decoder_ctx_ = GetDirectXForHardwareDecoding(); | |
video_blt_interfaces_.dx_device_ = dx_decoder_ctx_.dx_device_out; | |
DWORD input_stream_count = 0; | |
DWORD output_stream_count = 0; | |
decoder->GetStreamCount(&input_stream_count, &output_stream_count); | |
SB_DCHECK(1 == input_stream_count); | |
SB_DCHECK(1 == output_stream_count); | |
ComPtr<IMFAttributes> attributes = decoder->GetAttributes(); | |
HRESULT hr = attributes->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT, | |
kMinimumOutputSampleCount); | |
CheckResult(hr); | |
UINT32 value = 0; | |
hr = attributes->GetUINT32(MFT_SUPPORT_DYNAMIC_FORMAT_CHANGE, &value); | |
SB_DCHECK(hr == S_OK || hr == MF_E_ATTRIBUTENOTFOUND); | |
// TODO: handle the MFT_SUPPORT_DYNAMIC_FORMAT_CHANGE correctly. | |
// SB_DCHECK(value == TRUE); | |
// Enables DirectX video acceleration for video decoding. | |
decoder->SendMessage(MFT_MESSAGE_SET_D3D_MANAGER, | |
reinterpret_cast<ULONG_PTR>( | |
dx_decoder_ctx_.dxgi_device_manager_out.Get())); | |
ComPtr<IMFMediaType> output_type = decoder->GetCurrentOutputType(); | |
SB_DCHECK(output_type); | |
UINT32 width; | |
UINT32 height; | |
hr = MFGetAttributeSize(output_type.Get(), MF_MT_FRAME_SIZE, | |
&width, &height); | |
display_aperture_.left = 0; | |
display_aperture_.top = 0; | |
display_aperture_.right = width; | |
display_aperture_.bottom = height; | |
if (FAILED(hr)) { | |
SB_NOTREACHED() << "could not get width & height, hr = " << hr; | |
return; | |
} | |
ComPtr<IMFAttributes> output_attributes = | |
decoder->GetOutputStreamAttributes(); | |
// The decoder must output textures that are bound to shader resources, | |
// or we can't draw them later via ANGLE. | |
hr = output_attributes->SetUINT32( | |
MF_SA_D3D11_BINDFLAGS, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_DECODER); | |
SB_DCHECK(SUCCEEDED(hr)); | |
dx_decoder_ctx_.dx_device_out.As(&video_blt_interfaces_.video_device_); | |
D3D11_VIDEO_PROCESSOR_CONTENT_DESC video_processor_desc = {}; | |
video_processor_desc.InputFrameFormat = | |
D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; | |
// Presumably changing the input/output frame rate would | |
// allow us to use the VideoProcessor to decide which frames to drop | |
// But we already have common code that does this, so presumably | |
// by setting the input and output frame rate the same, we instruct | |
// the VideoProcessor to give us one output for every input frame. | |
video_processor_desc.InputFrameRate.Numerator = 60; | |
video_processor_desc.InputFrameRate.Denominator = 1; | |
video_processor_desc.OutputFrameRate = video_processor_desc.InputFrameRate; | |
SbWindowOptions window_options; | |
SbWindowSetDefaultOptions(&window_options); | |
video_processor_desc.OutputWidth = window_options.size.width; | |
video_processor_desc.OutputHeight = window_options.size.height; | |
video_processor_desc.InputWidth = video_processor_desc.OutputWidth; | |
video_processor_desc.InputHeight = video_processor_desc.OutputHeight; | |
video_processor_desc.OutputFrameRate.Numerator = 60; | |
video_processor_desc.OutputFrameRate.Denominator = 1; | |
video_processor_desc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL; | |
hr = video_blt_interfaces_.video_device_->CreateVideoProcessorEnumerator( | |
&video_processor_desc, &video_blt_interfaces_.video_processor_enum_); | |
CheckResult(hr); | |
hr = video_blt_interfaces_.video_device_->CreateVideoProcessor( | |
video_blt_interfaces_.video_processor_enum_.Get(), 0, | |
&video_blt_interfaces_.video_processor_); | |
CheckResult(hr); | |
ComPtr<ID3D11DeviceContext> device_context; | |
dx_decoder_ctx_.dx_device_out->GetImmediateContext(&device_context); | |
device_context.As(&video_blt_interfaces_.video_context_); | |
video_blt_interfaces_.video_context_->VideoProcessorSetStreamFrameFormat( | |
video_blt_interfaces_.video_processor_.Get(), | |
0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); | |
// https://msdn.microsoft.com/en-us/library/windows/desktop/hh447754(v=vs.85).aspx | |
// "for example, if you provide your own pixel shader for the video | |
// processor, you might want to disable the driver's automatic | |
// processing." | |
// We do have our own pixel shader, so we do want to disable anything | |
// like this. | |
video_blt_interfaces_.video_context_-> | |
VideoProcessorSetStreamAutoProcessingMode( | |
video_blt_interfaces_.video_processor_.Get(), 0, false); | |
} | |
bool TryWrite(const scoped_refptr<InputBuffer>& buff) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
const bool write_ok = impl_->TryWriteInputBuffer(buff, 0); | |
return write_ok; | |
} | |
void WriteEndOfStream() SB_OVERRIDE { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
impl_->Drain(); | |
ComPtr<IMFSample> sample; | |
ComPtr<IMFMediaType> media_type; | |
while (VideoFrameFactory::frames_in_flight() < kSampleAllocatorFramesMax && | |
impl_->ProcessAndRead(&sample, &media_type)) { | |
if (media_type) { | |
OnNewOutputType(media_type); | |
} | |
if (sample) { | |
Consume(sample); | |
} | |
} | |
} | |
VideoFramePtr ProcessAndRead(bool* too_many_outstanding_frames) SB_OVERRIDE { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
SB_DCHECK(too_many_outstanding_frames); | |
*too_many_outstanding_frames = | |
VideoFrameFactory::frames_in_flight() >= kSampleAllocatorFramesMax; | |
if (!*too_many_outstanding_frames) { | |
ComPtr<IMFSample> sample; | |
ComPtr<IMFMediaType> media_type; | |
impl_->ProcessAndRead(&sample, &media_type); | |
if (media_type) { | |
OnNewOutputType(media_type); | |
} | |
if (sample) { | |
Consume(sample); | |
} | |
} | |
if (output_queue_.empty()) { | |
return NULL; | |
} | |
VideoFramePtr output = output_queue_.front(); | |
output_queue_.pop(); | |
return output; | |
} | |
void Reset() SB_OVERRIDE { | |
impl_->Reset(); | |
std::queue<VideoFramePtr> empty; | |
output_queue_.swap(empty); | |
thread_checker_.Detach(); | |
} | |
// The object is single-threaded and is driven by a dedicated thread. | |
// However the thread may gets destroyed and re-created over the life time of | |
// this object. We enforce that certain member functions can only called | |
// from one thread while still allows this object to be driven by different | |
// threads by: | |
// 1. The |thread_checker_| is initially created without attaching to any | |
// thread. | |
// 2. When a thread is destroyed, Reset() will be called which in turn calls | |
// Detach() on the |thread_checker_| to allow the object to attach to a | |
// new thread. | |
::starboard::shared::starboard::ThreadChecker thread_checker_; | |
std::queue<VideoFramePtr> output_queue_; | |
const SbMediaVideoCodec codec_; | |
scoped_ptr<DecryptingDecoder> impl_; | |
RECT display_aperture_; | |
HardwareDecoderContext dx_decoder_ctx_; | |
VideoBltInterfaces video_blt_interfaces_; | |
}; | |
} // anonymous namespace. | |
scoped_ptr<AbstractWin32VideoDecoder> AbstractWin32VideoDecoder::Create( | |
SbMediaVideoCodec codec, | |
SbDrmSystem drm_system) { | |
return scoped_ptr<AbstractWin32VideoDecoder>( | |
new AbstractWin32VideoDecoderImpl(codec, drm_system)); | |
} | |
} // namespace win32 | |
} // namespace shared | |
} // namespace starboard |