| // Copyright 2017 The Cobalt Authors. 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 <functional> |
| |
| #include "starboard/common/log.h" |
| #include "starboard/shared/win32/dx_context_video_decoder.h" |
| #include "starboard/shared/win32/error_utils.h" |
| #include "starboard/shared/win32/hardware_decode_target_internal.h" |
| |
| // Include this after all other headers to avoid introducing redundant |
| // definitions from other header files. |
| // For CODECAPI_* |
| #include <codecapi.h> // NOLINT(build/include_order) |
| // For DXVA_ModeVP9_VLD_Profile0 |
| #include <initguid.h> // NOLINT(build/include_order) |
| |
| namespace starboard { |
| namespace shared { |
| namespace win32 { |
| |
| namespace { |
| |
| using Microsoft::WRL::ComPtr; |
| using ::starboard::shared::starboard::player::filter::VideoFrame; |
| using std::placeholders::_1; |
| using std::placeholders::_2; |
| |
| // 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 (if the decoder is not forced to use |
| // max resolution resources). |
| const int kMaxOutputSamples = 10; |
| |
| // Throttle the number of queued inputs to control memory usage. |
| const int kMaxInputSamples = 15; |
| |
| // Prime the VP9 decoder for a certain number of output frames to reduce |
| // hitching at the start of playback. |
| const int kVp9PrimingFrameCount = 10; |
| |
| // Decode targets are cached for reuse. Ensure the cache size is large enough |
| // to accommodate the depth of the presentation swap chain, otherwise the |
| // video textures may be updated before or while they are actually rendered. |
| const int kDecodeTargetCacheSize = 2; |
| |
| // Allocate decode targets at the maximum size to allow them to be reused for |
| // all resolutions. This minimizes hitching during resolution changes. |
| #ifdef ENABLE_VP9_8K_SUPPORT |
| const int kMaxDecodeTargetWidth = 7680; |
| const int kMaxDecodeTargetHeight = 4320; |
| #else // ENABLE_VP9_8K_SUPPORT |
| const int kMaxDecodeTargetWidth = 3840; |
| const int kMaxDecodeTargetHeight = 2160; |
| #endif // ENABLE_VP9_8K_SUPPOR |
| |
| DEFINE_GUID(DXVA_ModeVP9_VLD_Profile0, |
| 0x463707f8, |
| 0xa1d0, |
| 0x4585, |
| 0x87, |
| 0x6d, |
| 0x83, |
| 0xaa, |
| 0x6d, |
| 0x60, |
| 0xb8, |
| 0x9e); |
| |
| DEFINE_GUID(DXVA_ModeVP9_VLD_10bit_Profile2, |
| 0xa4c749ef, |
| 0x6ecf, |
| 0x48aa, |
| 0x84, |
| 0x48, |
| 0x50, |
| 0xa7, |
| 0xa1, |
| 0x16, |
| 0x5f, |
| 0xf7); |
| |
| scoped_ptr<MediaTransform> CreateVideoTransform( |
| const GUID& decoder_guid, |
| const GUID& input_guid, |
| const GUID& output_guid, |
| const IMFDXGIDeviceManager* device_manager) { |
| scoped_ptr<MediaTransform> media_transform(new MediaTransform(decoder_guid)); |
| if (!media_transform->HasValidTransform()) { |
| // Decoder Transform setup failed |
| return scoped_ptr<MediaTransform>().Pass(); |
| } |
| media_transform->EnableInputThrottle(true); |
| |
| ComPtr<IMFAttributes> attributes = media_transform->GetAttributes(); |
| if (!attributes) { |
| // Decoder Transform setup failed |
| return scoped_ptr<MediaTransform>().Pass(); |
| } |
| |
| UINT32 is_d3d_aware_attribute = false; |
| HRESULT hr = attributes->GetUINT32(MF_SA_D3D_AWARE, &is_d3d_aware_attribute); |
| if (SUCCEEDED(hr) && is_d3d_aware_attribute) { |
| // Ignore the return value, an error is expected when running in Session 0. |
| hr = media_transform->SendMessage(MFT_MESSAGE_SET_D3D_MANAGER, |
| ULONG_PTR(device_manager)); |
| if (FAILED(hr)) { |
| SB_LOG(WARNING) << "Unable to set device manager for d3d aware decoder, " |
| "disabling DXVA."; |
| hr = attributes->SetUINT32(CODECAPI_AVDecVideoAcceleration_H264, FALSE); |
| if (FAILED(hr)) { |
| SB_LOG(WARNING) << "Unable to disable DXVA."; |
| return scoped_ptr<MediaTransform>().Pass(); |
| } |
| } else { |
| hr = attributes->SetUINT32(CODECAPI_AVDecVideoAcceleration_H264, TRUE); |
| if (FAILED(hr)) { |
| SB_LOG(INFO) << "Unable to enable DXVA for video decoder."; |
| } |
| } |
| } else { |
| SB_LOG(WARNING) << "Video decoder is not D3D aware, decoding may be slow."; |
| } |
| |
| // Tell the decoder to allocate resources for the maximum resolution in |
| // order to minimize glitching on resolution changes. |
| if (FAILED(attributes->SetUINT32(MF_MT_DECODER_USE_MAX_RESOLUTION, 1))) { |
| return scoped_ptr<MediaTransform>().Pass(); |
| } |
| |
| ComPtr<IMFMediaType> input_type; |
| if (FAILED(MFCreateMediaType(&input_type)) || !input_type) { |
| return scoped_ptr<MediaTransform>().Pass(); |
| } |
| 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 || input_guid == MFVideoFormat_AV1) { |
| // 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, |
| kMaxDecodeTargetWidth, |
| kMaxDecodeTargetHeight)); |
| } |
| media_transform->SetInputType(input_type); |
| |
| media_transform->SetOutputTypeBySubType(output_guid); |
| |
| return media_transform.Pass(); |
| } |
| |
| class VideoFrameImpl : public VideoFrame { |
| public: |
| VideoFrameImpl(int64_t timestamp, std::function<void(VideoFrame*)> release_cb) |
| : VideoFrame(timestamp), release_cb_(release_cb) { |
| SB_DCHECK(release_cb_); |
| } |
| ~VideoFrameImpl() { release_cb_(this); } |
| |
| private: |
| std::function<void(VideoFrame*)> release_cb_; |
| }; |
| |
| } // namespace |
| |
| VideoDecoder::VideoDecoder( |
| SbMediaVideoCodec video_codec, |
| SbPlayerOutputMode output_mode, |
| SbDecodeTargetGraphicsContextProvider* graphics_context_provider, |
| SbDrmSystem drm_system, |
| bool is_hdr_supported) |
| : video_codec_(video_codec), |
| graphics_context_provider_(graphics_context_provider), |
| drm_system_(drm_system), |
| is_hdr_supported_(is_hdr_supported) { |
| SB_DCHECK(output_mode == kSbPlayerOutputModeDecodeToTexture); |
| |
| HardwareDecoderContext hardware_context = GetDirectXForHardwareDecoding(); |
| d3d_device_ = hardware_context.dx_device_out; |
| device_manager_ = hardware_context.dxgi_device_manager_out; |
| if (!d3d_device_ || !device_manager_) { |
| return; |
| } |
| |
| HRESULT hr = d3d_device_.As(&video_device_); |
| if (FAILED(hr)) { |
| return; |
| } |
| |
| ComPtr<ID3D11DeviceContext> d3d_context; |
| d3d_device_->GetImmediateContext(d3d_context.GetAddressOf()); |
| d3d_context.As(&video_context_); |
| } |
| |
| VideoDecoder::~VideoDecoder() { |
| SB_DCHECK(thread_checker_.CalledOnValidThread()); |
| Reset(); |
| ShutdownCodec(); |
| } |
| |
| // static |
| bool VideoDecoder::IsHardwareVp9DecoderSupported(bool is_hdr_required) { |
| static bool s_first_time_update = true; |
| static bool s_is_vp9_supported = false; |
| static bool s_is_hdr_supported = false; |
| |
| const UINT D3D11_VIDEO_DECODER_CAPS_UNSUPPORTED = 0x10; |
| |
| if (s_first_time_update) { |
| ComPtr<ID3D11Device> d3d11_device; |
| HRESULT hr = D3D11CreateDevice( |
| nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, |
| D3D11_SDK_VERSION, d3d11_device.GetAddressOf(), nullptr, nullptr); |
| if (FAILED(hr)) { |
| return false; |
| } |
| ComPtr<ID3D11VideoDevice1> video_device; |
| if (FAILED(d3d11_device.As(&video_device))) { |
| return false; |
| } |
| const DXGI_RATIONAL kFps = {30, 1}; // 30 fps = 30/1 |
| const UINT kBitrate = 0; |
| UINT caps_profile_0 = 0; |
| if (SUCCEEDED(video_device->GetVideoDecoderCaps(&DXVA_ModeVP9_VLD_Profile0, |
| 3840, 2160, &kFps, kBitrate, |
| NULL, &caps_profile_0))) { |
| s_is_vp9_supported = |
| caps_profile_0 != D3D11_VIDEO_DECODER_CAPS_UNSUPPORTED; |
| } |
| |
| UINT caps_profile_2 = 0; |
| if (SUCCEEDED(video_device->GetVideoDecoderCaps( |
| &DXVA_ModeVP9_VLD_10bit_Profile2, 3840, 2160, &kFps, kBitrate, NULL, |
| &caps_profile_2))) { |
| s_is_hdr_supported = |
| caps_profile_2 != D3D11_VIDEO_DECODER_CAPS_UNSUPPORTED; |
| } |
| s_first_time_update = false; |
| } |
| return is_hdr_required ? s_is_vp9_supported && s_is_hdr_supported |
| : s_is_vp9_supported; |
| } |
| |
| size_t VideoDecoder::GetPrerollFrameCount() const { |
| return kMaxOutputSamples; |
| } |
| |
| size_t VideoDecoder::GetMaxNumberOfCachedFrames() const { |
| return kMaxOutputSamples; |
| } |
| |
| void VideoDecoder::Initialize(const DecoderStatusCB& decoder_status_cb, |
| const ErrorCB& error_cb) { |
| SB_DCHECK(thread_checker_.CalledOnValidThread()); |
| SB_DCHECK(!decoder_status_cb_); |
| SB_DCHECK(decoder_status_cb); |
| SB_DCHECK(!error_cb_); |
| SB_DCHECK(error_cb); |
| decoder_status_cb_ = decoder_status_cb; |
| error_cb_ = error_cb; |
| if (video_device_) { |
| InitializeCodec(); |
| } |
| if (!decoder_) { |
| error_cb_(kSbPlayerErrorDecode, "Cannot initialize codec."); |
| } |
| } |
| |
| void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) { |
| SB_DCHECK(thread_checker_.CalledOnValidThread()); |
| SB_DCHECK(input_buffers.size() == 1); |
| SB_DCHECK(input_buffers[0]); |
| SB_DCHECK(decoder_status_cb_); |
| EnsureDecoderThreadRunning(); |
| if (error_occured_.load()) { |
| return; |
| } |
| const auto& input_buffer = input_buffers[0]; |
| if (TryUpdateOutputForHdrVideo(input_buffer->video_stream_info())) { |
| ScopedLock lock(thread_lock_); |
| thread_events_.emplace_back( |
| new Event{Event::kWriteInputBuffer, input_buffer}); |
| } else { |
| error_cb_(kSbPlayerErrorCapabilityChanged, |
| "HDR sink lost while HDR video playing."); |
| } |
| } |
| |
| void VideoDecoder::WriteEndOfStream() { |
| SB_DCHECK(thread_checker_.CalledOnValidThread()); |
| SB_DCHECK(decoder_status_cb_); |
| EnsureDecoderThreadRunning(); |
| if (error_occured_.load()) { |
| return; |
| } |
| |
| 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(); |
| error_occured_.store(false); |
| |
| // If the previous priming hasn't finished, restart it. This happens rarely |
| // as it is only triggered when a seek is requested immediately after video is |
| // started. |
| if (priming_output_count_ > 0) { |
| priming_output_count_ = kVp9PrimingFrameCount; |
| } |
| |
| decoder_status_cb_(kReleaseAllFrames, nullptr); |
| if (decoder_) { |
| decoder_->Reset(); |
| } |
| } |
| |
| SbDecodeTarget VideoDecoder::GetCurrentDecodeTarget() { |
| auto decode_target = CreateDecodeTarget(); |
| |
| if (SbDecodeTargetIsValid(decode_target)) { |
| if (SbDecodeTargetIsValid(current_decode_target_)) { |
| prev_decode_targets_.emplace_back(current_decode_target_); |
| } |
| current_decode_target_ = decode_target; |
| } |
| if (SbDecodeTargetIsValid(current_decode_target_)) { |
| // Add a reference for the caller. |
| current_decode_target_->AddRef(); |
| } |
| return current_decode_target_; |
| } |
| |
| 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) { |
| // Try reusing the previous decode target to avoid the performance hit of |
| // creating a new texture. |
| SB_DCHECK(prev_decode_targets_.size() <= kDecodeTargetCacheSize + 1); |
| if (prev_decode_targets_.size() >= kDecodeTargetCacheSize) { |
| decode_target = prev_decode_targets_.front(); |
| prev_decode_targets_.pop_front(); |
| auto hardware_decode_target = |
| reinterpret_cast<HardwareDecodeTargetPrivate*>(decode_target); |
| if (!hardware_decode_target->Update( |
| d3d_device_, video_device_, video_context_, video_enumerator_, |
| video_processor_, video_sample, video_area, is_hdr_supported_)) { |
| // The cached decode target was not compatible; just release it. |
| SbDecodeTargetRelease(decode_target); |
| decode_target = kSbDecodeTargetInvalid; |
| } |
| } |
| |
| if (!SbDecodeTargetIsValid(decode_target)) { |
| decode_target = new HardwareDecodeTargetPrivate( |
| d3d_device_, video_device_, video_context_, video_enumerator_, |
| video_processor_, video_sample, video_area, is_hdr_supported_); |
| auto hardware_decode_target = |
| reinterpret_cast<HardwareDecodeTargetPrivate*>(decode_target); |
| if (!hardware_decode_target->d3d_texture) { |
| error_cb_(kSbPlayerErrorDecode, |
| "Failed to allocate texture with error code: " + |
| ::starboard::shared::win32::HResultToString( |
| hardware_decode_target->create_texture_2d_h_result)); |
| decode_target = kSbDecodeTargetInvalid; |
| } |
| } |
| |
| // Release the video_sample before releasing the reset lock. |
| video_sample.Reset(); |
| } |
| return decode_target; |
| } |
| |
| void VideoDecoder::InitializeCodec() { |
| scoped_ptr<MediaTransform> media_transform; |
| |
| // 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, device_manager_.Get()); |
| priming_output_count_ = 0; |
| if (!media_transform) { |
| SB_LOG(WARNING) << "H264 hardware decoder creation failed."; |
| return; |
| } |
| break; |
| } |
| case kSbMediaVideoCodecVp9: { |
| if (IsHardwareVp9DecoderSupported()) { |
| media_transform = |
| CreateVideoTransform(CLSID_MSVPxDecoder, MFVideoFormat_VP90, |
| MFVideoFormat_NV12, device_manager_.Get()); |
| priming_output_count_ = kVp9PrimingFrameCount; |
| } |
| if (!media_transform) { |
| SB_LOG(WARNING) << "VP9 hardware decoder creation failed."; |
| return; |
| } |
| break; |
| } |
| case kSbMediaVideoCodecAv1: { |
| media_transform = |
| CreateVideoTransform(MFVideoFormat_AV1, MFVideoFormat_AV1, |
| MFVideoFormat_NV12, device_manager_.Get()); |
| priming_output_count_ = 0; |
| if (!media_transform) { |
| SB_LOG(WARNING) << "AV1 hardware decoder creation failed."; |
| return; |
| } |
| 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; |
| SB_DCHECK(transform); |
| 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(); |
| SB_DCHECK(attributes); |
| CheckResult( |
| attributes->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT, |
| static_cast<UINT32>(GetMaxNumberOfCachedFrames()))); |
| |
| 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(); |
| SB_DCHECK(output_attributes); |
| 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 = kMaxDecodeTargetWidth; |
| content_desc.InputHeight = kMaxDecodeTargetHeight; |
| content_desc.OutputFrameRate.Numerator = 60; |
| content_desc.OutputFrameRate.Denominator = 1; |
| content_desc.OutputWidth = kMaxDecodeTargetWidth; |
| content_desc.OutputHeight = kMaxDecodeTargetHeight; |
| 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())); |
| SB_DCHECK(video_context_); |
| 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_)); |
| SB_DCHECK(thread_outputs_.empty()); |
| if (!decoder_) { |
| return; |
| } |
| |
| // Work around a VP9 decoder crash. All IMFSamples and anything that may |
| // reference them indirectly (the d3d texture in SbDecodeTarget) must be |
| // released before releasing the IMFTransform. Do this on the render thread |
| // since graphics resources are being released. |
| graphics_context_provider_->gles_context_runner( |
| graphics_context_provider_, &VideoDecoder::ReleaseDecodeTargets, this); |
| |
| // Microsoft recommends stalling to let other systems release their |
| // references to the IMFSamples. |
| if (video_codec_ == kSbMediaVideoCodecVp9) { |
| SbThreadSleep(150'000); |
| } |
| decoder_.reset(); |
| video_processor_.Reset(); |
| video_enumerator_.Reset(); |
| } |
| |
| // static |
| void VideoDecoder::ReleaseDecodeTargets(void* context) { |
| VideoDecoder* this_ptr = static_cast<VideoDecoder*>(context); |
| while (!this_ptr->prev_decode_targets_.empty()) { |
| SbDecodeTargetRelease(this_ptr->prev_decode_targets_.front()); |
| this_ptr->prev_decode_targets_.pop_front(); |
| } |
| if (SbDecodeTargetIsValid(this_ptr->current_decode_target_)) { |
| SbDecodeTargetRelease(this_ptr->current_decode_target_); |
| this_ptr->current_decode_target_ = kSbDecodeTargetInvalid; |
| } |
| } |
| |
| 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 WriteInputBuffers or WriteEndOfStream again. |
| SB_DCHECK(!decoder_thread_stopped_); |
| |
| if (!SbThreadIsValid(decoder_thread_)) { |
| if (!decoder_) { |
| error_cb_(kSbPlayerErrorDecode, "Decoder is not valid."); |
| return; |
| } |
| 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)); |
| int64_t sample_time = ConvertWin32TimeToUsec(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 new VideoFrameImpl( |
| sample_time, std::bind(&VideoDecoder::DeleteVideoFrame, this, _1)); |
| } |
| |
| void VideoDecoder::DeleteVideoFrame(VideoFrame* video_frame) { |
| ScopedLock lock(thread_lock_); |
| for (auto iter = thread_outputs_.begin(); iter != thread_outputs_.end(); |
| ++iter) { |
| if (iter->time == video_frame->timestamp()) { |
| thread_outputs_.erase(iter); |
| break; |
| } |
| } |
| } |
| |
| void VideoDecoder::DecoderThreadRun() { |
| std::list<std::unique_ptr<Event> > priming_events; |
| std::unique_ptr<Event> event; |
| bool is_end_of_stream = false; |
| |
| while (!decoder_thread_stop_requested_) { |
| int outputs_to_process = 1; |
| bool wrote_input = false; |
| bool read_output = false; |
| |
| // 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(1000); |
| } else { |
| switch (event->type) { |
| case Event::kWriteInputBuffer: |
| SB_DCHECK(event->input_buffer != nullptr); |
| if (error_occured_) { |
| event.reset(); |
| break; |
| } |
| if (decoder_->TryWriteInputBuffer(event->input_buffer, 0)) { |
| if (priming_output_count_ > 0) { |
| // Save this event for the actual playback. |
| priming_events.emplace_back(event.release()); |
| } |
| // The event was successfully processed. Discard it. |
| event.reset(); |
| wrote_input = true; |
| } 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: |
| if (priming_output_count_ > 0) { |
| // Finish priming when eos is encountered. |
| priming_output_count_ = 0; |
| thread_lock_.Acquire(); |
| while (!priming_events.empty()) { |
| thread_events_.emplace_front(priming_events.back().release()); |
| priming_events.pop_back(); |
| } |
| // Also append the eos event. |
| thread_events_.emplace_back(event.release()); |
| thread_lock_.Release(); |
| decoder_->Reset(); |
| break; |
| } |
| event.reset(); |
| decoder_->Drain(); |
| is_end_of_stream = true; |
| wrote_input = 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(); |
| bool input_full = thread_events_.size() >= kMaxInputSamples; |
| bool output_full = thread_outputs_.size() >= GetMaxNumberOfCachedFrames(); |
| thread_lock_.Release(); |
| |
| Status status = input_full ? kBufferFull : kNeedMoreInput; |
| decoder_status_cb_(status, nullptr); |
| |
| if (output_full) { |
| // Wait for the active samples to be consumed before polling for more. |
| break; |
| } |
| |
| ComPtr<IMFSample> sample; |
| ComPtr<IMFMediaType> media_type; |
| bool hasError; |
| decoder_->ProcessAndRead(&sample, &media_type, &hasError); |
| if (hasError) { |
| error_occured_.exchange(true); |
| error_cb_(kSbPlayerErrorDecode, "Something went wrong in decoding."); |
| break; |
| } |
| if (media_type) { |
| UpdateVideoArea(media_type); |
| } |
| if (sample) { |
| if (priming_output_count_ > 0) { |
| // Ignore the output samples while priming the decoder. |
| if (--priming_output_count_ == 0) { |
| // Replay all the priming events once priming is finished. |
| if (event != nullptr) { |
| priming_events.emplace_back(event.release()); |
| } |
| thread_lock_.Acquire(); |
| while (!priming_events.empty()) { |
| thread_events_.emplace_front(priming_events.back().release()); |
| priming_events.pop_back(); |
| } |
| thread_lock_.Release(); |
| decoder_->Reset(); |
| } |
| } else { |
| decoder_status_cb_(status, CreateVideoFrame(sample)); |
| } |
| read_output = true; |
| } else if (is_end_of_stream) { |
| decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame()); |
| return; |
| } |
| } |
| |
| if (!wrote_input && !read_output) { |
| // Throttle decode loop since no I/O was possible. |
| SbThreadSleep(1000); |
| } |
| } |
| } |
| |
| // 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 |