实现 DirectShow 虚拟 Camera 驱动

2023-05-16

今天我们要实现一个虚拟 Camera 驱动。有这个驱动,在 播放软件(如 VLC)、视频会议软件、主播视频制作软件(如 OBS)中,就可以播放、加入我们的各种特制内容了。

先看看实现后的效果:

在 OBS 中使用我们的 Camera:

 

 

 在 Vlc 中播放使用我们的 Camera:

 

主要实现步骤

说是驱动,其实与真正的物理摄像头驱动是不一样的。我们买的物理摄像头,是通过 USB 与电脑连接,使用 UVC(USB Video Class)规范实现。

在 Windows 平台,实现虚拟 Camera,更简单的方法是基于 DirectShow 实现一个应用层的 Capture Source Filter,而且大部分 Windows 平台的视频软件都会适配 DirectShow Capture。

这篇文章假定你已经了解 DirectShow 的基本框架、工作原理,并且有一点的实践经验。在此基础上,通过这篇文章,能够了解到虚拟 Camera 的实现必要工作,并通过下面的基本步骤,可以完成一个真正的可以工作的虚拟 Camera。

通过实践总结下来,实现虚拟 Camera 需要以下几步:

  • 实现 IMediaFilter、IPin,实现基本的Pin 管理,图像输出
  • 实现 IKsPropertySet,声明 Pin 的类型(Capture、Preview)
  • 实现 IAMStreamConfig,支持 Camera 配置,如分辨率,帧率
  • 实现 IPropertyPage,支持配置的 Sheet(对话框),比如输入虚拟数据源的地址
  • 实现 ISpecifyPropertyPages,对外声明,本 Filter 支持的配置的 Sheet
  • 实现 Capture 的注册,注册为 Camera 设备,让其他软件能够找到你

实现 IMediaFilter、IPin

实现 IMediaFilter、IPin,是实现 DirectShow Source Filter 的基本任务。可以参考我的另外两篇文章:

播放器插件实现系列 —— DirectShow 之 SourceFilter_Fighting Horse的博客-CSDN博客

基于 DirectShow 实现 SourceFilter 常见问题分析_Fighting Horse的博客-CSDN博客

需要说明的是,Camera 中可用的视频格式是有限的。除了未压缩的 RGB、YUV 格式,只支持 MJPG 格式。这是行业的常规标准,也是出于成本考虑,支持视频编码的摄像头肯定要贵一些。

因此如果输入源是视频文件(一般是 H264 编码),想要虚拟为摄像头,就要考虑其他方案了,否则使用 Camera 的软件基本上用不了你的 Camera。

实现 IKsPropertySet

通过接口 IKsPropertySet,声明 Pin 是 CAPTURE 类型的。

接口 IKsPropertySet 有三个方法:

MethodDescription
GetRetrieves a property identified by a property set GUID and a property ID.
QuerySupportedDetermines whether an object supports a specified property set.
SetSets a property identified by a property set GUID and a property ID.

 不支持任何设置操作:

// Set: Cannot set any properties.
HRESULT CMyCapturePin::Set(REFGUID guidPropSet, DWORD dwID,
    void *pInstanceData, DWORD cbInstanceData, void *pPropData, 
    DWORD cbPropData)
{
    return E_NOTIMPL;
}

只支持获取 catagory 属性:

// Get: Return the pin category (our only property). 
HRESULT CMyCapturePin::Get(
    REFGUID guidPropSet,   // Which property set.
    DWORD dwPropID,        // Which property in that set.
    void *pInstanceData,   // Instance data (ignore).
    DWORD cbInstanceData,  // Size of the instance data (ignore).
    void *pPropData,       // Buffer to receive the property data.
    DWORD cbPropData,      // Size of the buffer.
    DWORD *pcbReturned     // Return the size of the property.
)
{
    if (guidPropSet != AMPROPSETID_Pin) 
        return E_PROP_SET_UNSUPPORTED;
    if (dwPropID != AMPROPERTY_PIN_CATEGORY)
        return E_PROP_ID_UNSUPPORTED;
    if (pPropData == NULL && pcbReturned == NULL)
        return E_POINTER;
    if (pcbReturned)
        *pcbReturned = sizeof(GUID);
    if (pPropData == NULL)  // Caller just wants to know the size.
        return S_OK;
    if (cbPropData < sizeof(GUID)) // The buffer is too small.
        return E_UNEXPECTED;
    *(GUID *)pPropData = PIN_CATEGORY_CAPTURE;
    return S_OK;
}

还是只支持 CATAGORY 属性,只读:

// QuerySupported: Query whether the pin supports the specified property.
HRESULT CMyCapturePin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID,
    DWORD *pTypeSupport)
{
    if (guidPropSet != AMPROPSETID_Pin)
        return E_PROP_SET_UNSUPPORTED;
    if (dwPropID != AMPROPERTY_PIN_CATEGORY)
        return E_PROP_ID_UNSUPPORTED;
    if (pTypeSupport)
        // We support getting this property, but not setting it.
        *pTypeSupport = KSPROPERTY_SUPPORT_GET; 
    return S_OK;
}

实现 Capture 的注册

只是声明 Pin 的类型,并不能让其他应用觉得你是一个 Camera。这比较令人泄气,毕竟没有什么比在其他应用中的看到我们的存在更令人兴奋了。

所以这一步是很关键的,当完成这一步之后,我们可以在其他应用中可以间接的操作我们的 Camera,调试我们的代码。

与一般 DirectShow Filter 的注册不一样的是,Capture Filter 还需要注册到 VideoInputDeviceCategory 中。

IFilterMapper2* fm = 0;
hr = CreateComObject(CLSID_FilterMapper2, IID_IFilterMapper2, fm);
if (SUCCEEDED(hr))
{
    if (bRegister)
    {
        IMoniker* pMoniker = 0;
        REGFILTER2 rf2;
        rf2.dwVersion = 1;
        rf2.dwMerit = MERIT_DO_NOT_USE;
        rf2.cPins = 1;
        rf2.rgPins = sudMyPin;
        // this is the name that actually shows up in VLC et al. weird
        hr = fm->RegisterFilter(CLSID_MyCamera, g_wszMyCamera, &pMoniker, &CLSID_VideoInputDeviceCategory, NULL, &rf2);
        pMoniker->Release();
    }
    else
    {
        hr = fm->UnregisterFilter(&CLSID_VideoInputDeviceCategory, 0, CLSID_MyCamera);
    }
}

// release interface
//
if (fm)
    fm->Release();

从注册表中,可以找到注册的结果:

 

 

实现 IAMStreamConfig

通过接口 IAMStreamConfig,对外暴露图像格式的细节。与 IPin::EnumMediaTypes 不同,这里给出的是各种配置参数的范围、可选值,也支持配置各种参数的值。

IAMStreamConfig::GetFormat
The GetFormat method retrieves the current or preferred output format.
IAMStreamConfig::GetNumberOfCapabilities
The GetNumberOfCapabilities method retrieves the number of format capabilities that this pin supports.
IAMStreamConfig::GetStreamCaps
The GetStreamCaps method retrieves a set of format capabilities.
IAMStreamConfig::SetFormat
The SetFormat method sets the output format on the pin.
HRESULT STDMETHODCALLTYPE CMyCapturePin::GetNumberOfCapabilities(int* piCount, int* piSize)
{
    *piCount = 1;
    *piSize = sizeof(VIDEO_STREAM_CONFIG_CAPS); // VIDEO_STREAM_CONFIG_CAPS is an MS struct
    return S_OK;
}

 外部获取各种配置参数的范围、可选值

HRESULT STDMETHODCALLTYPE CMyCapturePin::GetStreamCaps(int iIndex, AM_MEDIA_TYPE** pmt, BYTE* pSCC)
{
    CAutoLock cAutoLock(m_pFilter->pStateLock());
    HRESULT hr = GetMediaType(&m_mt); // setup then re-use m_mt ... why not?
    // some are indeed shared, apparently.
    if (FAILED(hr))
    {
        return hr;
    }

    *pmt = CreateMediaType(&m_mt); // a windows lib method, also does a copy for us
    if (*pmt == NULL) return E_OUTOFMEMORY;


    DECLARE_PTR(VIDEO_STREAM_CONFIG_CAPS, pvscc, pSCC);

    /*
      most of these are listed as deprecated by msdn... yet some still used, apparently. odd.
    */

    pvscc->VideoStandard = AnalogVideo_None;
    pvscc->InputSize.cx = m_info->format.video.width;
    pvscc->InputSize.cy = m_info->format.video.height;

    // most of these values are fakes..
    pvscc->MinCroppingSize.cx = m_info->format.video.width;
    pvscc->MinCroppingSize.cy = m_info->format.video.height;

    pvscc->MaxCroppingSize.cx = m_info->format.video.width;
    pvscc->MaxCroppingSize.cy = m_info->format.video.height;

    pvscc->CropGranularityX = 1;
    pvscc->CropGranularityY = 1;
    pvscc->CropAlignX = 1;
    pvscc->CropAlignY = 1;

    pvscc->MinOutputSize.cx = m_info->format.video.width;
    pvscc->MinOutputSize.cy = m_info->format.video.height;
    pvscc->MaxOutputSize.cx = m_info->format.video.width;
    pvscc->MaxOutputSize.cy = m_info->format.video.height;
    pvscc->OutputGranularityX = 1;
    pvscc->OutputGranularityY = 1;

    pvscc->StretchTapsX = 1; // We do 1 tap. I guess...
    pvscc->StretchTapsY = 1;
    pvscc->ShrinkTapsX = 1;
    pvscc->ShrinkTapsY = 1;

    pvscc->MinFrameInterval = 500000; // the larger default is actually the MinFrameInterval, not the max
    pvscc->MaxFrameInterval = 500000000; // 0.02 fps :) [though it could go lower, really...]

    pvscc->MinBitsPerSecond = (LONG)1 * 1 * 8 * m_info->format.video.frame_rate; // if in 8 bit mode 1x1. I guess.
    pvscc->MaxBitsPerSecond = (LONG)m_info->format.video.width * m_info->format.video.height * 32 * m_info->format.video.frame_rate + 44; // + 44 header size? + the palette?

    return hr;
}

 外部获取当前媒体格式:

HRESULT STDMETHODCALLTYPE CMyCapturePin::GetFormat(AM_MEDIA_TYPE** ppmt)
{
    CAutoLock cAutoLock(m_pFilter->pStateLock());
    if (!m_bFormatAlreadySet) {
        HRESULT hr = GetMediaType(&m_mt); // setup with index "0" kind of the default/preferred...I guess...
        if (FAILED(hr))
        {
            return hr;
        }
    }
    *ppmt = CreateMediaType(&m_mt); // windows internal method, also does copy
    return S_OK;
}

外部配置媒体格式:

HRESULT STDMETHODCALLTYPE CMyCapturePin::SetFormat(AM_MEDIA_TYPE* pmt)
{
    CAutoLock cAutoLock(m_pFilter->pStateLock());

    // I *think* it can go back and forth, then.  You can call GetStreamCaps to enumerate, then call
    // SetFormat, then later calls to GetMediaType/GetStreamCaps/EnumMediatypes will all "have" to just give this one
    // though theoretically they could also call EnumMediaTypes, then Set MediaType, and not call SetFormat
    // does flash call both? what order for flash/ffmpeg/vlc calling both?
    // LODO update msdn

    // "they" [can] call this...see msdn for SetFormat

    // NULL means reset to default type...
    if (pmt != NULL)
    {
        if (pmt->formattype != FORMAT_VideoInfo)  // FORMAT_VideoInfo == {CLSID_KsDataTypeHandlerVideo} 
            return E_FAIL;

        // LODO I should do more here...http://msdn.microsoft.com/en-us/library/dd319788.aspx I guess [meh]
        // LODO should fail if we're already streaming... [?]

        if (CheckMediaType((CMediaType*)pmt) != S_OK) {
            return E_FAIL; // just in case :P [FME...]
        }
        VIDEOINFOHEADER* pvi = (VIDEOINFOHEADER*)pmt->pbFormat;

        // for FMLE's benefit, only accept a setFormat of our "final" width [force setting via registry I guess, otherwise it only shows 80x60 whoa!]	    
        // flash media live encoder uses setFormat to determine widths [?] and then only displays the smallest? huh?
        if (pvi->bmiHeader.biWidth != m_info->format.video.width ||
            pvi->bmiHeader.biHeight != m_info->format.video.height)
        {
            return E_INVALIDARG;
        }

        // ignore other things like cropping requests for now...

        // now save it away...for being able to re-offer it later. We could use Set MediaType but we're just being lazy and re-using m_mt for many things I guess
        m_mt = *pmt;

    }

    IPin* pin;
    ConnectedTo(&pin);
    if (pin)
    {
        IFilterGraph* pGraph = m_pFilter->GetFilterGraph();
        HRESULT res = pGraph->Reconnect(this);
        if (res != S_OK) // LODO check first, and then just re-use the old one?
            return res; // else return early...not really sure how to handle this...since we already set m_mt...but it's a pretty rare case I think...
          // plus ours is a weird case...
    }
    else {
        // graph hasn't been built yet...
        // so we're ok with "whatever" format they pass us, we're just in the setup phase...
    }



    // success of some type
    if (pmt == NULL) {
        m_bFormatAlreadySet = FALSE;
    }
    else {
        m_bFormatAlreadySet = TRUE;
    }

    return S_OK;
}

 

实现 IPropertyPage

通过 IPropertyPage 提供自定义的 Camera 配置或者信息展示的 UI 页面,其他应用也可以给用户展示该页面。

对于虚拟 Camera 来说,自定义的配置的最大用处是让用户输入图像数据的来源。比如将一个视频文件虚拟为 Camera,那么就要做一个 UI 界面,让用户选择他的视频文件。这个工作就在这一步完成。

如上图,这里我们只实现了一个输入框。

DirectShow baseclasses 提供了 CPropertyPage 类帮助实现 IPropertyPage,我们只需要实现下列方法,就可以工作了:

virtual HRESULT OnConnect(IUnknown* pUnk);
virtual HRESULT OnActivate();
virtual INT_PTR OnReceiveMessage(HWND hwnd,
    UINT uMsg, WPARAM wParam, LPARAM lParam);
virtual HRESULT OnApplyChanges();
virtual HRESULT OnDisconnect();

不过,还需要我们自己添加对话框资源,开发过 MFC 界面程序的程序员,应该都知道。不知道也很简单,通过拖拽一些控件就能够完成了。需要说明的是,新建对话框时,选择 IDD_OLE_PROPPAGE_SMALL。

在进一步实现 CPropertyPage 前,还需要定义并实现自己的读写配置值的接口:

DEFINE_GUID(IID_ICameraConfig,
    0x608b220, 0xe2f8, 0x4ddb, 0x99, 0xb6, 0xbf, 0xe5, 0x54, 0x25, 0xa9, 0xee);

interface ICameraConfig : public IUnknown
{
    STDMETHOD(GetUrl)(LPCTSTR* psUrl) = 0;
    STDMETHOD(SetUrl)(LPCTSTR sUrl) = 0;
};

实现该接口:

STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetUrl(LPCTSTR* psUrl)
{
    *psUrl = m_URL;
    return S_OK;
}

STDMETHODIMP_(HRESULT __stdcall) CMyCamera::SetUrl(LPCTSTR sUrl)
{
    lstrcpyW(m_URL, sUrl);
    Load(m_URL, NULL);
    return S_OK;
}

接下来就是实现 CPropertyPage 的几个方法了:

在连接时,查询并保存配置接口 ICameraConfig 对象:

HRESULT CMyPropertyPage::OnConnect(IUnknown* pUnk)
{
    if (pUnk == NULL)
    {
        return E_POINTER;
    }
    ASSERT(m_pConfig == NULL);
    return pUnk->QueryInterface(IID_ICameraConfig,
        reinterpret_cast<void**>(&m_pConfig));
}

在激活时,对话框窗口已经创建了,可以填入当前的配置值:

HRESULT CMyPropertyPage::OnActivate()
{
    ASSERT(m_pConfig != NULL);
    LPCTSTR url;
    HRESULT hr = m_pConfig->GetUrl(&url);
    if (SUCCEEDED(hr))
    {
        SendDlgItemMessage(m_Dlg, IDC_URL, WM_SETTEXT, 0, (LPARAM)url);
    }
    return hr;
}

在收到 Windows 消息时,比如文本框文字改变时,标记配置值被修改了:

INT_PTR CMyPropertyPage::OnReceiveMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_COMMAND:
        if (wParam == MAKEWPARAM(IDC_URL, EN_CHANGE)) {
            if (IsWindowVisible(m_hwnd))
                SetDirty();
        }
        break;
    } // Switch.

    // Let the parent class handle the message.
    return CBasePropertyPage::OnReceiveMessage(hwnd, uMsg, wParam, lParam);
}

void CMyPropertyPage::SetDirty()
{
    m_bDirty = TRUE;
    if (m_pPageSite)
    {
        m_pPageSite->OnStatusChange(PROPPAGESTATUS_DIRTY);
    }
}

在用户点击“确认”或者"应用" 时,写入新的配置值:

HRESULT CMyPropertyPage::OnApplyChanges() {
    ASSERT(m_pConfig != NULL);
    TCHAR url[MAX_PATH];
    SendDlgItemMessage(m_Dlg, IDC_URL, WM_GETTEXT, MAX_PATH, (LPARAM)url);
    HRESULT hr = m_pConfig->SetUrl(url);
    return hr;
}

在断开连接时,释放配置接口 ICameraConfig 对象:

HRESULT CMyPropertyPage::OnDisconnect()
{
    if (m_pConfig)
    {
        m_pConfig->Release();
        m_pConfig = NULL;
    }
    return S_OK;
}

实现 ISpecifyPropertyPages

只有 IPropertyPage 并没有展示出配置界面。还需要实现 ISpecifyPropertyPages 接口。该接口只有一个方法:

ISpecifyPropertyPages::GetPages
Retrieves a list of property pages that can be displayed in this object's property sheet.

该方法返回一个 GUID 数组,但是应该返回什么 GUID,文档中说得很模糊。

在尝试了很久之后,才明白,需要将上面的 IPropertyPage 对象像 DirectShow Filter 一样注册,然后在这里返回对应的 CLSID。

注册 CMyPropertyPage:

CFactoryTemplate g_Templates[] = 
{
    ......,
    {
        L"My Camera Property Page",
        & CLSID_MyCameraPropertyPage,
        CMyPropertyPage::CreateInstance,
        NULL,
        NULL
    }
};

实现 GetPages 方法: 

STDMETHODIMP_(HRESULT __stdcall) CMyCamera::GetPages(CAUUID* pPages)
{
    pPages->cElems = 1;
    pPages->pElems = (GUID*)CoTaskMemAlloc(sizeof(GUID));
    if (pPages->pElems == NULL)
    {
        return E_OUTOFMEMORY;
    }
    pPages->pElems[0] = CLSID_MyCameraPropertyPage;
    return S_OK;
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实现 DirectShow 虚拟 Camera 驱动 的相关文章

  • 将预览帧转换为位图

    我知道这个主题已经在黑板上出现过很多次了 但无论如何我都无法让它发挥作用 我想将预览中的视图帧保存为 jpeg 文件 它看起来或多或少 代码被简化 没有额外的逻辑 异常等 像这样 public void onPreviewFrame byt
  • Zxing扫描仪相机变焦

    我已经尝试解决这个问题3天了 但仍然没有找到答案 我想在通过 Zxing 扫描仪扫描二维码时向相机添加 ZOOM 构建 gradle implementation me dm7 barcodescanner zxing 1 9 8 Xml
  • 检测所有摄像头android

    我有一个使用 Camera2 API 来获取相机预览的应用程序 我想在手机的镜头之间进行选择 在我的代码中 我使用以下代码 CameraManager manager CameraManager activity getSystemServ
  • 如何消除 DirectShow 过滤器链中 1 秒的延迟? (使用Delphi和DSPACK)

    我有一个 Delphi 6 Pro 应用程序 它使用 DSPACK 组件库将音频从系统的首选音频输入设备发送到 Skype 我正在使用 TSampleGrabber 组件进入 Filter Graph 链 然后将音频缓冲区发送到 Skype
  • iphone如何通过Xcode拍照后进入图片库

    大家好 我正在实施以下代码来通过我的应用程序拍照 UIApplication sharedApplication keyWindow setRootViewController picker picker sourceType UIImag
  • Windows Mobile:通过 C# 使用手机摄像头

    我想展示手机摄像头在 WinForm 中的控件的图像 我的想法是我的应用程序就像相机的程序一样工作 我想显示图像 就像用户要拍照一样 我怎样才能做到这一点 我可以这样做吗 如果您需要更多详细信息 请询问我 谢谢你 不太确定您需要什么 但您可
  • Firemonkey相机组件太慢

    我有一个问题TCameraComponent在安卓上 我第一次运行该应用程序时 相机正常 但如果我停止TCameraComponent再次启动 相机变得太慢 对正在发生的事情有什么想法吗 CameraComponent1 Active fa
  • iOS:如何打开带有动画效果的相机?

    我想以动画效果打开相机 以便看起来相机仅在父屏幕中打开 我正在使用相机覆盖屏幕 在父屏幕中单击按钮事件时 相机覆盖屏幕正在打开 在相机覆盖屏幕中有一个取消按钮可以关闭相机 因此在再次关闭相机时我需要显示动画效果看起来现在相机在同一个父屏幕中
  • 从连接到计算机并在成像设备中列出的相机捕获图像

    我有一台佳能 EOS 1000D 当我将其连接到计算机时 它列在 控制面板 gt 成像设备 下 我想以编程方式拍照 我猜想成像设备中列出的所有设备都具有相同的接口 可能是 TWAIN 并且具有向它们发送命令的标准方法 TWAIN可以做到吗
  • AVLayerVideoGravityResize 在新设备、iOS 10 上不匹配?

    具有全屏实时预览功能的相机 previewLayer videoGravity AVLayerVideoGravityResize 制作图像 stillImageOutput captureStillImageAsynchronously
  • 如何在SceneKit中实现逼真的景深效果?

    我正在尝试渲染具有真实景深效果的帧 我已经尝试过景深属性camera节点 但它不会产生可用的结果 是否可以切换到景深效果的最大渲染质量 性能不是一个因素 我只需要渲染一帧 用户可以等待它 SceneKit 中逼真的景深效果 在SceneKi
  • Android相机无法从后台服务拍照

    我已经实现了一项从后台线程拍照的服务 但照片永远不会在我的任何设备上拍摄 这是代码 下面记录输出 public class PhotoCaptureService extends Service private static final S
  • Android ACTION_IMAGE_CAPTURE 与内存中的 EXTRA_OUTPUT

    当我打电话时用相机拍照时 File file new File getFilesDir getAbsolutePath myImage jpg Uri outputFileUri Uri fromFile file cameraIntent
  • 相机 API:跨设备问题

    我正在开发一个相机应用程序 基本上作为消息传递应用程序的一部分来附加图像等 该应用程序需要适用于 gt SDK 2 2 并且 I can t尽可能多地使用默认的 Android 相机 因为 该应用程序的性质决定了图像不应保存到磁盘上 一些
  • ipad 2相机支持检测

    我有一个使用以下宏的应用程序 define IS IPAD UIDevice currentDevice respondsToSelector selector userInterfaceIdiom UIDevice currentDevi
  • 有机会通过 Android NDK 相机访问来减少快门时间吗?

    我编写了一个 Android 应用程序 可以从相机捕获实时预览 快门时间短很重要 至少应该是恒定的 目前我使用以下代码来实现低快门时间 Parameters params camera getParameters params setSce
  • 使用畸变从图像平面计算相机矢量

    我正在尝试使用相机模型来重建可以使用某些相机及其 外部 内部 参数拍摄的图像 这一点我没有任何问题 现在我想添加扭曲 正如它们中所描述的那样OpenCV https docs opencv org 4 x dc dbb tutorial p
  • java.lang.RuntimeException:release()后调用的方法

    If i am 不使用 相机 release in 表面被破坏 then 无法从另一个 Activity 再次启动 CameraActivity 简而言之 得到不幸的是应用程序已停止 错误 即使不释放相机 但如果我确实点击了 主页 按钮 来
  • Ionic-Angular.js 拍照并发送到服务器:空图像

    因此 我设法使用自定义指令通过 Angular js 将图像上传到我的服务器 我还成功地实现了 Cordova 的相机功能 现在我尝试连接两者 但是当将图像发送到服务器时 它们被存储为空 我认为问题在于我使用输入字段来获取图像 并且它获取了
  • 如何将相机中的图像保存到 iPhone 图库中的特定文件夹?

    嘿 我是 iPhone 新手 最近我一直在尝试制作一个应用程序 基本上 我想要做的是 如果用户将从相机捕获任何图像 那么它应该保存在设备库中 我知道如何将照片保存在图库中 它对我有用 但我无法将所有捕获的图像保存到设备图库中的特定文件夹 例

随机推荐

  • ARM-MPU内存保护单元详解

    ARM MPU 详解 简介 MPU Memory Protection Unit 内存保护单元 本文主要讲 armv7 m 架构 架构下的 MPU 在 armv7 m 架构下 xff0c Cortex M3 和 Cortex M4 处理器对
  • 玩转doxygen 之RT-THREAD

    玩转doxygen 之RT THREAD 文章目标 经常会看到小伙伴们遇到怎么写函数注释头疼 xff0c 以及如何生成漂亮的代码注释文档头疼 据我了解 xff0c 目前C语言中的代码注释规则有且只有一种比较常用 xff0c 就是doxyge
  • STM32如何将文件放到内部flash里面

    STM32如何将文件放到内部flash里面 背景介绍 上一篇讲到如何将STM32的FLASH改成文件系统 xff1a 如何不用外设在STM32片上FLASH做一个文件系统 https club rt thread org ask artic
  • 营运型手游开发、测试、正式的三阶段开发架构

    在手机游戏的畅销排行榜上 xff0c 可以看到大多数的游戏都是营运型的游戏 所谓的营运型游戏 xff0c 指的是游戏的开发并不是上架后就结束 xff0c 而是需要持续的配合游戏营运的需求 xff0c 进行游戏的更新 内容调整以及后续内容的开
  • 【github】【action】如何给软件包添加CI集成

    github action 如何给软件包添加CI集成 简介 github有自己的CI集成工具 action 很少有小伙伴关注到 xff0c 如果你有自己的软件包 xff0c 想要对其进行维护的话 xff0c 添加CI集成能够方便你快速验证你
  • Access 标准表达式中数据类型不匹配

    Access 标准表达式中数据类型不匹配 Access标准表达式中数据类型不匹配 今天在做一个小程序时 要求用到Access数据库 在调试运行一个SELECT语句时 老是提示标准表达式中数据类型不匹配 弄了好久 原来发现是数据类型不匹配的问
  • c#中new一个对象以后,是否需要手动释放?

    c 中new一个对象以后 xff0c 是否需要手动释放 xff1f 2012 04 28 23 43 wshbfzdzb 分类 xff1a C NET 浏览723次 c 43 43 中 class1 a 61 new class1 需要在用
  • ARM M0+各种定时器驱动的编写

    systick 系统滴答时间 这个定时器之前的文章已经讲过 这个是一个递减的定时器 xff0c 有个模数寄存器 在此不多说 就是一个系统的模块 xff0c 这个模块是集成在ARM M0 43 内核中的 xff0c 其实主要是集成在NVIC
  • MG323所有命令使用

    AT 43 CGMR 61 OK AT 43 GMR 61 OK AT 43 GMR 12 210 10 05 00 OK AT 43 CGSN 351869042318140 OK AT 43 CIMI 460021734971641 O
  • BAT文件的常用语法

    bat文件中常用的命令有 xff1a echo 64 rem pause goto call if copy等 下面简要给出这几个命令的用法 1 echo命令 echo 表示显示此命令后的字符 例如echoHello World choHe
  • c++ http请求,json解析

    一 文章内容 解决c 43 43 http请求以及对返回结果json串进行解析 xff0c 使用jsoncpp库 二 安装jsoncpp插件 vs2015通过NuGet直接安装jsoncpp到项目下 安装好之后 xff0c 会在项目下有个p
  • Linux安装Oracle12c操作手册

    1 基本环境 服务器 xff1a 64位 16核CPU 384G内存 16T硬盘 操作系统 xff1a CentOS 7 4 Oracle版本 xff1a 12c 版本号12 1 0 2 0 2 安装必要的软件包 查看rpm包是否安装 xf
  • tiny6410按键驱动总结

    写了7个版本的按键驱动 xff1a 1 查询法 xff1a 在应用程序的while循环里不停的调用read函数读取按键值 xff0c 太耗费CPU资源了 2 中断发 xff1a 同样是在一个while循环里不停的调用read函数读按键值 x
  • linux中shell的常用命令

    shell 常用命令 什么是shell xff1f shell 也是操作系统中的一个软件 xff0c 它包在 linux 内核的外面 为用户和内核之间的交互提供了一个接口 一 diff命令 diff b表示忽略空格 xff0c B表示忽略空
  • 空心杯电机学习笔记

    空心杯电机学习笔记 1 空心杯电机 xff08 直流电机 xff09 的硬核拆解2 空心杯电机的驱动模块学习 xff08 1 xff09 无人机飞控原理学习的流程介绍 xff08 空心杯四旋翼DIY xff09 xff08 2 xff09
  • C++ 中“空引用”与“空指针”的区别

    网络上有很多讨论C 43 43 的 引用 与 指针 的区别的文章 xff0c 谈到区别 xff0c 其中有一条 xff1a 引用不能为空 xff08 NULL xff09 xff0c 引用必须与合法的存储单元关联 xff0c 指针则可以是N
  • 关于 std::vector 的下标越界检查

    当要获取 std vector 的第 n 个元素 xff0c 下面几种方式都可以 xff1a std vector lt int gt vec size t n 61 1 int amp i 61 vec n int amp j 61 ve
  • 【第三篇】 基于 Qt 的 REST 网络框架

    本文是 Qt 框架性开发实践 基础框架篇 的第三篇 本文所讲的内容已经开源 xff0c 你可以在 这里 找到源代码 在 Java 以及其他语言中 xff0c 处理与后端的 HTTP 通讯 xff0c 有专门的工具库 xff0c 使用起来特别
  • Qt/QML 实现图片圆角剪切效果

    在很多 UI 设计中 xff0c 需要将图片按照一定的方式整形 比如下面的 VIP 图片就是用一个圆形剪切原始图片 xff0c 形成的效果 其实它的原始图片是这样的 xff1a 要在 QML 中实现这样的效果 xff0c 可以使用 Opac
  • 实现 DirectShow 虚拟 Camera 驱动

    今天我们要实现一个虚拟 Camera 驱动 有这个驱动 xff0c 在 播放软件 xff08 如 VLC xff09 视频会议软件 主播视频制作软件 xff08 如 OBS xff09 中 xff0c 就可以播放 加入我们的各种特制内容了