blob: 6245704ca006a79efdea785f13b5faa06ea9ec36 [file] [log] [blame]
// 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/video_decoder.h"
#include "starboard/log.h"
#include "starboard/shared/win32/decode_target_internal.h"
#include "starboard/shared/win32/dx_context_video_decoder.h"
#include "starboard/shared/win32/error_utils.h"
namespace starboard {
namespace shared {
namespace win32 {
namespace {
using Microsoft::WRL::ComPtr;
// Limit the number of active output samples to control memory usage.
// NOTE: Higher numbers may result in increased dropped frames when the video
// resolution changes during playback.
const int kMaxOutputSamples = 5;
// This structure is used to facilitate creation of decode targets in the
// appropriate graphics context.
struct CreateDecodeTargetContext {
VideoDecoder* video_decoder;
SbDecodeTarget out_decode_target;
};
scoped_ptr<MediaTransform> CreateVideoTransform(const GUID& decoder_guid,
const GUID& input_guid, const GUID& output_guid,
const SbWindowSize& window_size,
const IMFDXGIDeviceManager* device_manager) {
scoped_ptr<MediaTransform> transform(new MediaTransform(decoder_guid));
transform->SendMessage(MFT_MESSAGE_SET_D3D_MANAGER,
ULONG_PTR(device_manager));
ComPtr<IMFMediaType> input_type;
CheckResult(MFCreateMediaType(&input_type));
CheckResult(input_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video));
CheckResult(input_type->SetGUID(MF_MT_SUBTYPE, input_guid));
CheckResult(input_type->SetUINT32(MF_MT_INTERLACE_MODE,
MFVideoInterlace_Progressive));
if (input_guid == MFVideoFormat_VP90) {
// Set the expected video resolution. Setting the proper resolution can
// mitigate a format change, but the decoder will adjust to the real
// resolution regardless.
CheckResult(MFSetAttributeSize(input_type.Get(), MF_MT_FRAME_SIZE,
window_size.width, window_size.height));
}
transform->SetInputType(input_type);
transform->SetOutputTypeBySubType(output_guid);
return transform.Pass();
}
} // namespace
VideoDecoder::VideoDecoder(
SbMediaVideoCodec video_codec,
SbPlayerOutputMode output_mode,
SbDecodeTargetGraphicsContextProvider* graphics_context_provider,
SbDrmSystem drm_system)
: video_codec_(video_codec),
graphics_context_provider_(graphics_context_provider),
drm_system_(drm_system),
host_(nullptr),
decoder_thread_(kSbThreadInvalid),
decoder_thread_stop_requested_(false),
decoder_thread_stopped_(false),
current_decode_target_(kSbDecodeTargetInvalid),
prev_decode_target_(kSbDecodeTargetInvalid) {
SB_DCHECK(output_mode == kSbPlayerOutputModeDecodeToTexture);
HardwareDecoderContext hardware_context = GetDirectXForHardwareDecoding();
d3d_device_ = hardware_context.dx_device_out;
device_manager_ = hardware_context.dxgi_device_manager_out;
CheckResult(d3d_device_.As(&video_device_));
ComPtr<ID3D11DeviceContext> d3d_context;
d3d_device_->GetImmediateContext(d3d_context.GetAddressOf());
CheckResult(d3d_context.As(&video_context_));
InitializeCodec();
}
VideoDecoder::~VideoDecoder() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
Reset();
ShutdownCodec();
ScopedLock lock(decode_target_lock_);
SbDecodeTargetRelease(current_decode_target_);
SbDecodeTargetRelease(prev_decode_target_);
}
void VideoDecoder::SetHost(Host* host) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(host != nullptr);
SB_DCHECK(host_ == nullptr);
host_ = host;
}
void VideoDecoder::Initialize(const Closure& error_cb) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(!error_cb_.is_valid());
SB_DCHECK(error_cb.is_valid());
error_cb_ = error_cb;
}
void VideoDecoder::WriteInputBuffer(
const scoped_refptr<InputBuffer>& input_buffer) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(input_buffer);
SB_DCHECK(host_ != nullptr);
EnsureDecoderThreadRunning();
ScopedLock lock(thread_lock_);
thread_events_.emplace_back(
new Event{ Event::kWriteInputBuffer, input_buffer });
}
void VideoDecoder::WriteEndOfStream() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(host_ != nullptr);
EnsureDecoderThreadRunning();
ScopedLock lock(thread_lock_);
thread_events_.emplace_back(new Event{ Event::kWriteEndOfStream });
}
void VideoDecoder::Reset() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
StopDecoderThread();
// Make sure all output samples have been released before flushing the
// decoder. Be sure to Acquire the mutexes in the same order as
// CreateDecodeTarget to avoid possible deadlock.
outputs_reset_lock_.Acquire();
thread_lock_.Acquire();
thread_outputs_.clear();
thread_lock_.Release();
outputs_reset_lock_.Release();
decoder_->Reset();
}
SbDecodeTarget VideoDecoder::GetCurrentDecodeTarget() {
// Ensure the decode target is created on the render thread.
CreateDecodeTargetContext decode_target_context = { this };
graphics_context_provider_->gles_context_runner(graphics_context_provider_,
&VideoDecoder::CreateDecodeTargetHelper, &decode_target_context);
ScopedLock lock(decode_target_lock_);
if (SbDecodeTargetIsValid(decode_target_context.out_decode_target)) {
SbDecodeTargetRelease(prev_decode_target_);
prev_decode_target_ = current_decode_target_;
current_decode_target_ = decode_target_context.out_decode_target;
}
if (SbDecodeTargetIsValid(current_decode_target_)) {
// Add a reference for the caller.
current_decode_target_->AddRef();
}
return current_decode_target_;
}
// static
void VideoDecoder::CreateDecodeTargetHelper(void* context) {
CreateDecodeTargetContext* target_context =
static_cast<CreateDecodeTargetContext*>(context);
target_context->out_decode_target =
target_context->video_decoder->CreateDecodeTarget();
}
SbDecodeTarget VideoDecoder::CreateDecodeTarget() {
RECT video_area;
ComPtr<IMFSample> video_sample;
// Don't allow a decoder reset (flush) while an IMFSample is
// alive. However, the decoder thread should be allowed to continue
// while the SbDecodeTarget is being created.
ScopedLock reset_lock(outputs_reset_lock_);
// Use the oldest output.
thread_lock_.Acquire();
if (!thread_outputs_.empty()) {
// This function should not remove output frames. However, it's possible
// for the same frame to be requested multiple times. To avoid re-creating
// SbDecodeTargets, release the video_sample once it is used to create
// an output frame. The next call to CreateDecodeTarget for the same frame
// will return kSbDecodeTargetInvalid, and |current_decode_target_| will
// be reused.
Output& output = thread_outputs_.front();
video_area = output.video_area;
video_sample.Swap(output.video_sample);
}
thread_lock_.Release();
SbDecodeTarget decode_target = kSbDecodeTargetInvalid;
if (video_sample != nullptr) {
ScopedLock target_lock(decode_target_lock_);
// Try reusing the previous decode target to avoid the performance hit of
// creating a new texture.
if (SbDecodeTargetIsValid(prev_decode_target_) &&
prev_decode_target_->Update(d3d_device_, video_device_,
video_context_, video_enumerator_, video_processor_,
video_sample, video_area)) {
decode_target = prev_decode_target_;
prev_decode_target_ = kSbDecodeTargetInvalid;
} else {
decode_target = new SbDecodeTargetPrivate(d3d_device_, video_device_,
video_context_, video_enumerator_, video_processor_,
video_sample, video_area);
}
// Release the video_sample before releasing the reset lock.
video_sample.Reset();
}
return decode_target;
}
void VideoDecoder::InitializeCodec() {
scoped_ptr<MediaTransform> media_transform;
SbWindowOptions window_options;
SbWindowSetDefaultOptions(&window_options);
// If this is updated then media_is_video_supported.cc also needs to be
// updated.
switch (video_codec_) {
case kSbMediaVideoCodecH264: {
media_transform = CreateVideoTransform(
CLSID_MSH264DecoderMFT, MFVideoFormat_H264, MFVideoFormat_NV12,
window_options.size, device_manager_.Get());
break;
}
case kSbMediaVideoCodecVp9: {
media_transform = CreateVideoTransform(
CLSID_MSVPxDecoder, MFVideoFormat_VP90, MFVideoFormat_NV12,
window_options.size, device_manager_.Get());
break;
}
default: { SB_NOTREACHED(); }
}
decoder_.reset(
new DecryptingDecoder("video", media_transform.Pass(), drm_system_));
MediaTransform* transform = decoder_->GetDecoder();
DWORD input_stream_count = 0;
DWORD output_stream_count = 0;
transform->GetStreamCount(&input_stream_count, &output_stream_count);
SB_DCHECK(1 == input_stream_count);
SB_DCHECK(1 == output_stream_count);
ComPtr<IMFAttributes> attributes = transform->GetAttributes();
CheckResult(attributes->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT,
kMaxOutputSamples));
UpdateVideoArea(transform->GetCurrentOutputType());
// The transform must output textures that are bound to shader resources,
// or we can't draw them later via ANGLE.
ComPtr<IMFAttributes> output_attributes =
transform->GetOutputStreamAttributes();
CheckResult(output_attributes->SetUINT32(MF_SA_D3D11_BINDFLAGS,
D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_DECODER));
// The resolution and framerate will adjust to the actual content data.
D3D11_VIDEO_PROCESSOR_CONTENT_DESC content_desc = {};
content_desc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
content_desc.InputFrameRate.Numerator = 60;
content_desc.InputFrameRate.Denominator = 1;
content_desc.InputWidth = window_options.size.width;
content_desc.InputHeight = window_options.size.height;
content_desc.OutputFrameRate.Numerator = 60;
content_desc.OutputFrameRate.Denominator = 1;
content_desc.OutputWidth = window_options.size.width;
content_desc.OutputHeight = window_options.size.height;
content_desc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL;
CheckResult(video_device_->CreateVideoProcessorEnumerator(
&content_desc, video_enumerator_.GetAddressOf()));
CheckResult(video_device_->CreateVideoProcessor(
video_enumerator_.Get(), 0, video_processor_.GetAddressOf()));
video_context_->VideoProcessorSetStreamFrameFormat(
video_processor_.Get(), MediaTransform::kStreamId,
D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
video_context_->VideoProcessorSetStreamAutoProcessingMode(
video_processor_.Get(), 0, false);
}
void VideoDecoder::ShutdownCodec() {
SB_DCHECK(!SbThreadIsValid(decoder_thread_));
decoder_.reset();
video_processor_.Reset();
video_enumerator_.Reset();
}
void VideoDecoder::EnsureDecoderThreadRunning() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
// NOTE: The video decoder thread will exit after processing the
// kWriteEndOfStream event. In this case, Reset must be called (which will
// then StopDecoderThread) before WriteInputBuffer or WriteEndOfStream again.
SB_DCHECK(!decoder_thread_stopped_);
if (!SbThreadIsValid(decoder_thread_)) {
SB_DCHECK(decoder_ != nullptr);
SB_DCHECK(thread_events_.empty());
decoder_thread_stop_requested_ = false;
decoder_thread_ =
SbThreadCreate(0, kSbThreadPriorityHigh, kSbThreadNoAffinity, true,
"VideoDecoder", &VideoDecoder::DecoderThreadEntry, this);
SB_DCHECK(SbThreadIsValid(decoder_thread_));
}
}
void VideoDecoder::StopDecoderThread() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
if (SbThreadIsValid(decoder_thread_)) {
decoder_thread_stop_requested_ = true;
SbThreadJoin(decoder_thread_, nullptr);
SB_DCHECK(decoder_thread_stopped_);
decoder_thread_stopped_ = false;
decoder_thread_ = kSbThreadInvalid;
}
thread_events_.clear();
}
void VideoDecoder::UpdateVideoArea(const ComPtr<IMFMediaType>& media) {
MFVideoArea video_area;
HRESULT hr = media->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE,
reinterpret_cast<UINT8*>(&video_area),
sizeof(video_area), nullptr);
if (SUCCEEDED(hr)) {
video_area_.left = video_area.OffsetX.value;
video_area_.top = video_area.OffsetY.value;
video_area_.right = video_area_.left + video_area.Area.cx;
video_area_.bottom = video_area_.top + video_area.Area.cy;
return;
}
UINT32 width;
UINT32 height;
hr = MFGetAttributeSize(media.Get(), MF_MT_FRAME_SIZE, &width, &height);
if (SUCCEEDED(hr)) {
video_area_.left = 0;
video_area_.top = 0;
video_area_.right = width;
video_area_.bottom = height;
return;
}
SB_NOTREACHED() << "Could not determine new video output resolution";
}
scoped_refptr<VideoFrame> VideoDecoder::CreateVideoFrame(
const ComPtr<IMFSample>& sample) {
// NOTE: All samples must be released before flushing the decoder. Since
// the host may hang onto VideoFrames that are created here, make them
// weak references to the actual sample.
LONGLONG win32_sample_time = 0;
CheckResult(sample->GetSampleTime(&win32_sample_time));
SbMediaTime sample_time = ConvertToMediaTime(win32_sample_time);
thread_lock_.Acquire();
thread_outputs_.emplace_back(sample_time, video_area_, sample);
thread_lock_.Release();
// The "native texture" for the VideoFrame is actually just the timestamp
// for the output sample.
return make_scoped_refptr(new VideoFrame(
video_area_.right, video_area_.bottom, sample_time,
new SbMediaTime(sample_time), this, &VideoDecoder::DeleteVideoFrame));
}
// static
void VideoDecoder::DeleteVideoFrame(void* context, void* native_texture) {
VideoDecoder* this_ptr = static_cast<VideoDecoder*>(context);
SbMediaTime* time_ptr = static_cast<SbMediaTime*>(native_texture);
SbMediaTime time = *time_ptr;
delete time_ptr;
ScopedLock lock(this_ptr->thread_lock_);
for (auto iter = this_ptr->thread_outputs_.begin();
iter != this_ptr->thread_outputs_.end(); ++iter) {
if (iter->time == time) {
this_ptr->thread_outputs_.erase(iter);
break;
}
}
}
void VideoDecoder::DecoderThreadRun() {
std::unique_ptr<Event> event;
bool is_end_of_stream = false;
while (!decoder_thread_stop_requested_) {
int outputs_to_process = 1;
// Process a new event or re-try the previous event.
if (event == nullptr) {
ScopedLock lock(thread_lock_);
if (!thread_events_.empty()) {
event.swap(thread_events_.front());
thread_events_.pop_front();
}
}
if (event == nullptr) {
SbThreadSleep(kSbTimeMillisecond);
} else {
switch (event->type) {
case Event::kWriteInputBuffer:
SB_DCHECK(event->input_buffer != nullptr);
if (decoder_->TryWriteInputBuffer(event->input_buffer, 0)) {
// The event was successfully processed. Discard it.
event.reset();
} else {
// The decoder must be full. Re-try the event on the next
// iteration. Additionally, try reading an extra output frame to
// start draining the decoder.
++outputs_to_process;
}
break;
case Event::kWriteEndOfStream:
event.reset();
decoder_->Drain();
is_end_of_stream = true;
break;
}
}
// Process output frame(s).
for (int outputs = 0; outputs < outputs_to_process; ++outputs) {
// NOTE: IMFTransform::ProcessOutput (called by decoder_->ProcessAndRead)
// may stall if the number of active IMFSamples would exceed the value of
// MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT.
thread_lock_.Acquire();
size_t output_count = thread_outputs_.size();
thread_lock_.Release();
if (output_count >= kMaxOutputSamples) {
// Wait for the active samples to be consumed.
host_->OnDecoderStatusUpdate(kBufferFull, nullptr);
SbThreadSleep(kSbTimeMillisecond);
continue;
}
ComPtr<IMFSample> sample;
ComPtr<IMFMediaType> media_type;
decoder_->ProcessAndRead(&sample, &media_type);
if (media_type) {
UpdateVideoArea(media_type);
}
if (sample) {
host_->OnDecoderStatusUpdate(kNeedMoreInput,
CreateVideoFrame(sample));
} else if (is_end_of_stream) {
host_->OnDecoderStatusUpdate(kBufferFull,
VideoFrame::CreateEOSFrame());
return;
} else {
host_->OnDecoderStatusUpdate(kNeedMoreInput, nullptr);
}
}
}
}
// static
void* VideoDecoder::DecoderThreadEntry(void* context) {
SB_DCHECK(context);
VideoDecoder* decoder = static_cast<VideoDecoder*>(context);
decoder->DecoderThreadRun();
decoder->decoder_thread_stopped_ = true;
return nullptr;
}
} // namespace win32
} // namespace shared
} // namespace starboard
namespace starboard {
namespace shared {
namespace starboard {
namespace player {
namespace filter {
// static
bool VideoDecoder::OutputModeSupported(SbPlayerOutputMode output_mode,
SbMediaVideoCodec codec,
SbDrmSystem drm_system) {
SB_UNREFERENCED_PARAMETER(codec);
SB_UNREFERENCED_PARAMETER(drm_system);
return output_mode == kSbPlayerOutputModeDecodeToTexture;
}
} // namespace filter
} // namespace player
} // namespace starboard
} // namespace shared
} // namespace starboard