diff --git a/src/softcamcore/DShowSoftcam.cpp b/src/softcamcore/DShowSoftcam.cpp index 30315d3..859eca4 100644 --- a/src/softcamcore/DShowSoftcam.cpp +++ b/src/softcamcore/DShowSoftcam.cpp @@ -8,6 +8,8 @@ #include #include +#include "Misc.h" + namespace { @@ -147,11 +149,33 @@ AM_MEDIA_TYPE* makeMediaType(int width, int height, float framerate) return amt; } +bool UseDefaultBlankImage = false; // Testing purpose only +int DefaultImageWidth = 0; // Testing purpose only +int DefaultImageHeight = 0; // Testing purpose only + +const char DefaultImageSuffix[] = "\\placeholder.png"; +const float DefaultFramerate = 60.0f; + } //namespace namespace softcam { +void Softcam::enableDefaultBlankImage(int width, int height) +{ + UseDefaultBlankImage = true; + DefaultImageWidth = width; + DefaultImageHeight = height; +} + +void Softcam::disableDefaultBlankImage() +{ + UseDefaultBlankImage = false; + DefaultImageWidth = 0; + DefaultImageHeight = 0; +} + + CUnknown * Softcam::CreateInstance( LPUNKNOWN lpunk, const GUID& clsid, @@ -166,15 +190,24 @@ CUnknown * Softcam::CreateInstance( Softcam::Softcam(LPUNKNOWN lpunk, const GUID& clsid, HRESULT *phr) : CSource(NAME("DirectShow Softcam"), lpunk, clsid), m_frame_buffer(FrameBuffer::open()), - m_valid(m_frame_buffer ? true : false), - m_width(m_frame_buffer.width()), - m_height(m_frame_buffer.height()), - m_framerate(m_frame_buffer.framerate()) + m_default_image( + m_frame_buffer ? DefaultImage{} : + UseDefaultBlankImage ? DefaultImage::makeBlankImage(DefaultImageWidth, DefaultImageHeight) : + DefaultImage::tryLoad(GetModuleDirectoryPath() + DefaultImageSuffix)), + m_valid(m_frame_buffer || m_default_image), + m_width(m_frame_buffer ? m_frame_buffer.width() : m_default_image ? m_default_image.width() : 0), + m_height(m_frame_buffer ? m_frame_buffer.height() : m_default_image ? m_default_image.height() : 0), + m_framerate(m_frame_buffer ? m_frame_buffer.framerate() : m_default_image ? DefaultFramerate : 0.0f) { - CAutoLock lock(&m_cStateLock); - - m_paStreams = new CSourceStream*[1]; - m_paStreams[0] = new SoftcamStream(phr, this, L"DirectShow Softcam Stream"); + LOG("ctor -> frame_buffer:%s(%dx%d) default_image:%s(%dx%d)\n", + m_frame_buffer ? "valid" : "none", m_frame_buffer.width(), m_frame_buffer.height(), + m_default_image ? "valid" : "none", m_default_image.width(), m_default_image.height()); + + // This code is okay though it may look strange as the return value is ignored. + // Calling the SoftcamStream constructor results in calling the CBaseOutputPin + // constructor which registers the instance to this Softcam instance by calling + // CSource::AddPin(). + (void)new SoftcamStream(phr, this, L"DirectShow Softcam Stream"); } @@ -335,7 +368,7 @@ FrameBuffer* Softcam::getFrameBuffer() return nullptr; } - CAutoLock lock(&m_cStateLock); + CAutoLock lock(&m_critsec); if (!m_frame_buffer) { auto fb = FrameBuffer::open(); @@ -360,10 +393,24 @@ FrameBuffer* Softcam::getFrameBuffer() void Softcam::releaseFrameBuffer() { - CAutoLock lock(&m_cStateLock); + CAutoLock lock(&m_critsec); m_frame_buffer.release(); } +const DefaultImage* +Softcam::getDefaultImage() +{ + CAutoLock lock(&m_critsec); + if (m_default_image) + { + return &m_default_image; + } + else + { + return nullptr; + } +} + SoftcamStream::SoftcamStream(HRESULT *phr, Softcam *pParent, LPCWSTR pPinName) : @@ -446,7 +493,14 @@ HRESULT SoftcamStream::FillBuffer(IMediaSample *pms) Timer::sleep(0.100f); const std::size_t size = calcDIBSize(m_width, m_height); - std::memcpy(pData, m_screenshot.get(), size); + if (m_screenshot) + { + std::memcpy(pData, m_screenshot.get(), size); + } + else if (auto def = getParent()->getDefaultImage()) + { + std::memcpy(pData, def->imageBits(), size); + } } CAutoLock lock(&m_critsec); diff --git a/src/softcamcore/DShowSoftcam.h b/src/softcamcore/DShowSoftcam.h index 4e18d82..8ff8b03 100644 --- a/src/softcamcore/DShowSoftcam.h +++ b/src/softcamcore/DShowSoftcam.h @@ -3,6 +3,7 @@ #include #include #include "FrameBuffer.h" +#include "DefaultImage.h" namespace softcam { @@ -33,8 +34,16 @@ class Softcam : public CSource, public IAMStreamConfig float framerate() const { return m_framerate; } void releaseFrameBuffer(); + const DefaultImage* getDefaultImage(); + + // Testing purpose only + static void enableDefaultBlankImage(int width, int height); + static void disableDefaultBlankImage(); + private: + CCritSec m_critsec; FrameBuffer m_frame_buffer; + DefaultImage m_default_image; const bool m_valid; const int m_width; const int m_height; diff --git a/src/softcamcore/DefaultImage.cpp b/src/softcamcore/DefaultImage.cpp new file mode 100644 index 0000000..c8d9a80 --- /dev/null +++ b/src/softcamcore/DefaultImage.cpp @@ -0,0 +1,115 @@ +#include "DefaultImage.h" + +#include +#include +#include +#include + + +#pragma comment(lib, "Windowscodecs.lib") + + +namespace { + +std::wstring ToWString(const std::string& str) +{ + size_t len = 0, converted = 0; + _locale_t loc = _create_locale(LC_ALL, ""); + _mbstowcs_s_l(&len, nullptr, 0, str.data(), str.size() + 1, loc); + std::wstring wstr(len, L'\0'); + _mbstowcs_s_l(&converted, (wchar_t*)wstr.data(), len, str.data(), _TRUNCATE, loc); + _free_locale(loc); + return std::wstring(wstr.c_str()); +} + +} + + +namespace softcam { + + +DefaultImage +DefaultImage::tryLoad(const std::string& file_path) +{ + if (!PathFileExistsA(file_path.c_str())) + { + return {}; + } + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) + { + return {}; + } + DefaultImage img; + IWICImagingFactory* factory = nullptr; + if (!FAILED(CoCreateInstance( + CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (void**)&factory))) + { + IWICBitmapDecoder* decoder = nullptr; + if (!FAILED(factory->CreateDecoderFromFilename( + ToWString(file_path).c_str(), nullptr, GENERIC_READ, + WICDecodeMetadataCacheOnDemand, &decoder))) + { + IWICBitmapFrameDecode* decode = nullptr; + if (!FAILED(decoder->GetFrame(0, &decode))) + { + IWICFormatConverter* converter = nullptr; + if (!FAILED(factory->CreateFormatConverter(&converter))) + { + do + { + if (FAILED(converter->Initialize( + decode, GUID_WICPixelFormat24bppBGR, WICBitmapDitherTypeNone, + nullptr, 0.0, WICBitmapPaletteTypeCustom))) + { + break; + } + UINT w, h; + if (FAILED(converter->GetSize(&w, &h))) + { + break; + } + if (w < 1 || w > 16384 || w % 4 != 0 || + h < 1 || h > 16384 || h % 4 != 0) + { + break; + } + auto bits = std::make_unique(h*w*3); + if (FAILED(converter->CopyPixels(nullptr, w*3, h*w*3, bits.get()))) + { + break; + } + img.m_valid = true; + img.m_width = (int)w; + img.m_height = (int)h; + img.m_bits = std::move(bits); + } while (false); + converter->Release(); + } + decode->Release(); + } + decoder->Release(); + } + factory->Release(); + } + CoUninitialize(); + return img; +} + +DefaultImage +DefaultImage::makeBlankImage(int width, int height) +{ + DefaultImage img; + img.m_valid = true; + img.m_width = width; + img.m_height = height; + + auto buffer_size = height * width * 3; + img.m_bits = std::make_unique(buffer_size); + std::memset(img.m_bits.get(), 0x00, buffer_size); + + return std::move(img); +} + + +} //namespace softcam diff --git a/src/softcamcore/DefaultImage.h b/src/softcamcore/DefaultImage.h new file mode 100644 index 0000000..c2e4e78 --- /dev/null +++ b/src/softcamcore/DefaultImage.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + + +namespace softcam { + + +/// Default image +class DefaultImage +{ + public: + static DefaultImage tryLoad(const std::string& file_path); + static DefaultImage makeBlankImage(int width, int height); + + explicit operator bool() const { return m_valid; } + + int width() const { return m_width; } + int height() const { return m_height; } + const void* imageBits() const { return m_bits.get(); } + + private: + bool m_valid = false; + int m_width = 0; + int m_height = 0; + std::unique_ptr m_bits; +}; + + +} //namespace softcam diff --git a/src/softcamcore/Misc.cpp b/src/softcamcore/Misc.cpp index a4af02d..c9b038b 100644 --- a/src/softcamcore/Misc.cpp +++ b/src/softcamcore/Misc.cpp @@ -1,10 +1,14 @@ #include "Misc.h" #include +#include #include #include +#pragma comment(lib, "shlwapi.lib") // PathRemoveFileSpecA + + namespace softcam { @@ -188,4 +192,30 @@ SharedMemory::unmap(void* ptr) } } +std::string +GetModuleDirectoryPath() +{ + // get module handle + MEMORY_BASIC_INFORMATION info; + if (0 < VirtualQueryEx( + GetCurrentProcess(), + (void*)&GetModuleDirectoryPath, + &info, + sizeof(info))) + { + HMODULE hmodule = (HMODULE)info.AllocationBase; + // get the file path + char buf[1024]; + if (0 < GetModuleFileNameA(hmodule, buf, sizeof(buf))) + { + // get the directory path + if (PathRemoveFileSpecA(buf)) + { + return buf; + } + } + } + return {}; +} + } //namespace softcam diff --git a/src/softcamcore/Misc.h b/src/softcamcore/Misc.h index 49d32c4..15457d4 100644 --- a/src/softcamcore/Misc.h +++ b/src/softcamcore/Misc.h @@ -2,6 +2,7 @@ #include #include +#include namespace softcam { @@ -69,4 +70,8 @@ class SharedMemory }; +/// Retrieve the directory path of this module (DLL/EXE) +std::string GetModuleDirectoryPath(); + + } //namespace softcam diff --git a/src/softcamcore/softcamcore.vcxproj b/src/softcamcore/softcamcore.vcxproj index 8dac4af..f84a05a 100644 --- a/src/softcamcore/softcamcore.vcxproj +++ b/src/softcamcore/softcamcore.vcxproj @@ -149,6 +149,7 @@ + @@ -156,6 +157,7 @@ + diff --git a/src/softcamcore/softcamcore.vcxproj.filters b/src/softcamcore/softcamcore.vcxproj.filters index c861811..05c3617 100644 --- a/src/softcamcore/softcamcore.vcxproj.filters +++ b/src/softcamcore/softcamcore.vcxproj.filters @@ -26,6 +26,9 @@ Header Files + + Header Files + @@ -43,5 +46,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/tests/core_tests/DShowSoftcamTest.cpp b/tests/core_tests/DShowSoftcamTest.cpp index 87b926e..5aebc73 100644 --- a/tests/core_tests/DShowSoftcamTest.cpp +++ b/tests/core_tests/DShowSoftcamTest.cpp @@ -57,6 +57,7 @@ class Softcam : public ::testing::Test { m_softcam->Release(); } + sc::Softcam::disableDefaultBlankImage(); } }; @@ -129,6 +130,87 @@ TEST_F(Softcam, AttributesNormal) EXPECT_EQ( m_softcam->framerate(), 60.0f ); } +TEST_F(Softcam, AttributesNoServerButWithDefaultImage) +{ + sc::Softcam::enableDefaultBlankImage(320, 240); + + HRESULT hr = 555; + m_softcam = (sc::Softcam*)sc::Softcam::CreateInstance(nullptr, SOME_GUID, &hr); + ASSERT_NE( m_softcam, nullptr ); + m_softcam->AddRef(); + + EXPECT_EQ( m_softcam->getFrameBuffer(), nullptr ); + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); + EXPECT_EQ( m_softcam->height(), 240 ); + EXPECT_EQ( m_softcam->framerate(), 60.0f ); +} + +TEST_F(Softcam, AttributesWithBothValidServerAndDefaultImage) +{ + sc::Softcam::enableDefaultBlankImage(160, 120); + auto fb = createFrameBufer(320, 240, 30); + + HRESULT hr = 555; + m_softcam = (sc::Softcam*)sc::Softcam::CreateInstance(nullptr, SOME_GUID, &hr); + ASSERT_NE( m_softcam, nullptr ); + m_softcam->AddRef(); + + EXPECT_NE( m_softcam->getFrameBuffer(), nullptr ); + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); + EXPECT_EQ( m_softcam->height(), 240 ); + EXPECT_EQ( m_softcam->framerate(), 30.0f ); +} + +TEST_F(Softcam, AttributesSwitchingFromDefaultImageToActualServer) +{ + sc::Softcam::enableDefaultBlankImage(320, 240); + + HRESULT hr = 555; + m_softcam = (sc::Softcam*)sc::Softcam::CreateInstance(nullptr, SOME_GUID, &hr); + ASSERT_NE( m_softcam, nullptr ); + m_softcam->AddRef(); + + EXPECT_EQ( m_softcam->getFrameBuffer(), nullptr ); + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); + EXPECT_EQ( m_softcam->height(), 240 ); + EXPECT_EQ( m_softcam->framerate(), 60.0f ); + + auto fb = createFrameBufer(320, 240, 30); + + EXPECT_NE( m_softcam->getFrameBuffer(), nullptr ); + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); + EXPECT_EQ( m_softcam->height(), 240 ); + EXPECT_EQ( m_softcam->framerate(), 60.0f ); +} + +TEST_F(Softcam, AttributesKeepingDefaultImageIfIncompatibleServer) +{ + sc::Softcam::enableDefaultBlankImage(320, 240); + + HRESULT hr = 555; + m_softcam = (sc::Softcam*)sc::Softcam::CreateInstance(nullptr, SOME_GUID, &hr); + ASSERT_NE( m_softcam, nullptr ); + m_softcam->AddRef(); + + EXPECT_EQ( m_softcam->getFrameBuffer(), nullptr ); + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); + EXPECT_EQ( m_softcam->height(), 240 ); + EXPECT_EQ( m_softcam->framerate(), 60.0f ); + + auto fb = createFrameBufer(300, 200, 30); // <-- + + EXPECT_EQ( m_softcam->getFrameBuffer(), nullptr ); // <-- + EXPECT_EQ( m_softcam->valid(), true ); + EXPECT_EQ( m_softcam->width(), 320 ); // <-- + EXPECT_EQ( m_softcam->height(), 240 ); // <-- + EXPECT_EQ( m_softcam->framerate(), 60.0f ); +} + TEST_F(Softcam, AttributesDeactivatedServer) { auto fb = createFrameBufer(320, 240, 60); @@ -921,5 +1003,43 @@ TEST_F(SoftcamStream, CSourceStreamGetMediaTypeNormal) checkMediaType320x240(&mt); } +TEST_F(SoftcamStream, getFrameBuffer_must_not_lock_the_filter_state) +{ + auto fb = createFrameBufer(320, 240, 60); + SetUpSoftcamStream(); + + CAutoLock lock(m_softcam->pStateLock()); // 1st lock + std::atomic done{false}; + std::thread th([&] + { + // A deadlock occurs if this code attempts to get lock. + m_softcam->getFrameBuffer(); + done = true; + }); + + for (int i = 0; !done && i < 20; i++) { sc::Timer::sleep(0.050f); } + ASSERT_TRUE( done ); + th.join(); +} + +TEST_F(SoftcamStream, releaseFrameBuffer_must_not_lock_the_filter_state) +{ + auto fb = createFrameBufer(320, 240, 60); + SetUpSoftcamStream(); + + CAutoLock lock(m_softcam->pStateLock()); // 1st lock + std::atomic done{false}; + std::thread th([&] + { + // A deadlock occurs if this code attempts to get lock. + m_softcam->releaseFrameBuffer(); + done = true; + }); + + for (int i = 0; !done && i < 20; i++) { sc::Timer::sleep(0.050f); } + ASSERT_TRUE( done ); + th.join(); +} + } //namespace diff --git a/tests/core_tests/DefaultImageTest.cpp b/tests/core_tests/DefaultImageTest.cpp new file mode 100644 index 0000000..c2f80f7 --- /dev/null +++ b/tests/core_tests/DefaultImageTest.cpp @@ -0,0 +1,33 @@ +#include +#include + + +namespace { +namespace sc = softcam; + + +TEST(DefaultImage, FileNotFound) { + auto img = sc::DefaultImage::tryLoad("not_a_valid_file_path"); + + EXPECT_FALSE( img ); + EXPECT_EQ( img.width(), 0 ); + EXPECT_EQ( img.height(), 0 ); + EXPECT_EQ( img.imageBits(), nullptr ); +} + +TEST(DefaultImage, BlankImage) { + auto img = sc::DefaultImage::makeBlankImage(320, 200); + + EXPECT_TRUE( img ); + EXPECT_EQ( img.width(), 320 ); + EXPECT_EQ( img.height(), 200 ); + + ASSERT_NE( img.imageBits(), nullptr ); + EXPECT_EQ( ((const std::uint8_t*)img.imageBits())[0], 0x00 ); + EXPECT_EQ( ((const std::uint8_t*)img.imageBits())[1], 0x00 ); + EXPECT_EQ( ((const std::uint8_t*)img.imageBits())[2], 0x00 ); + EXPECT_EQ( ((const std::uint8_t*)img.imageBits())[200*320*3-1], 0x00 ); +} + + +} //namespace diff --git a/tests/core_tests/MiscTest.cpp b/tests/core_tests/MiscTest.cpp index 8e28385..c8013a6 100644 --- a/tests/core_tests/MiscTest.cpp +++ b/tests/core_tests/MiscTest.cpp @@ -218,4 +218,18 @@ TEST(SharedMemory, MultipleOpenSucceeds) { EXPECT_GE( view3.size(), SHMEM_SIZE ); } +TEST(GetModuleDirectoryPath, NotEmpty) { + auto path = sc::GetModuleDirectoryPath(); + + EXPECT_FALSE( path.empty() ); +} + +TEST(GetModuleDirectoryPath, Consistent) { + auto path1 = sc::GetModuleDirectoryPath(); + auto path2 = sc::GetModuleDirectoryPath(); + + EXPECT_TRUE( path1 == path2 ); +} + + } //namespace diff --git a/tests/core_tests/core_tests.vcxproj b/tests/core_tests/core_tests.vcxproj index c807857..fae4c5f 100644 --- a/tests/core_tests/core_tests.vcxproj +++ b/tests/core_tests/core_tests.vcxproj @@ -45,6 +45,7 @@ $(Platform)\$(Configuration)\ +