GetOutputStatus 表示输出已准备就绪,ProcessOutput 表示 NEED_MORE_INPUT

Posted

技术标签:

【中文标题】GetOutputStatus 表示输出已准备就绪,ProcessOutput 表示 NEED_MORE_INPUT【英文标题】:GetOutputStatus says output is ready, ProcessOutput says NEED_MORE_INPUT 【发布时间】:2017-02-20 11:13:53 【问题描述】:

我陷入了这种情况,我得到了相互矛盾的信息:

hr = pDecoder->lpVtbl->ProcessInput(pDecoder, dwInputStreamID, pSample, dwFlags);
if (FAILED(hr) ... )
//ProcessInput went well, no warnings from here.

hr = pDecoder->lpVtbl->GetOutputStatus(pDecoder, &dwFlags);
if (SUCCEEDED(hr)) 
        if (dwFlags == MFT_OUTPUT_STATUS_SAMPLE_READY) 
            // I get to here, sample is ready, yay!
        
    
dwFlags = 0;

hr = pDecoder->lpVtbl->GetInputStatus(pDecoder, 0, &dwFlags);
if (SUCCEEDED(hr)) 
    if (dwFlags == MFT_INPUT_STATUS_ACCEPT_DATA) 
        //...
     else 
        // we go here, input does not accept more data it seems.
        // Sounds ok, we read the output that is ready and then we fill in more
    


dwFlags = 0;

hr = pDecoder->lpVtbl->ProcessOutput(pDecoder,
    dwFlags,
    1,
    pOutputSamples,
    &pdwStatus
);
if (FAILED(hr)) 
    if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) 
        // Ok, but why did GetOutputStatus say we were ready then?
    


// Calling GetOutputStatus to see whats going on
hr = pDecoder->lpVtbl->GetOutputStatus(pDecoder, &dwFlags);
if (SUCCEEDED(hr)) 
    if (dwFlags == MFT_OUTPUT_STATUS_SAMPLE_READY) 
        // nope.
     else 
        // Now dwFlags is 0.
    


hr = pDecoder->lpVtbl->GetInputStatus(pDecoder, 0, &dwFlags);
if (SUCCEEDED(hr)) 
    if (dwFlags == MFT_INPUT_STATUS_ACCEPT_DATA) 
        // this time we go here, we can now give more input again.
        // but we got no data from ProcessOutput
     else 
        //...
    


dwFlags = 0;

查看我发送到 processOutput 以填充它的数据媒体样本,它只是在缓冲区的开头写入一个空终止符 '\0',否则它不会写入任何输出。

GetOutputStatus

如果方法返回 MFT_OUTPUT_STATUS_SAMPLE_READY 标志,它 意味着您可以通过调用生成一个或多个输出样本 IMFTransform::ProcessOutput。

...

客户端设置有效媒体后 所有流上的类型,MFT 应该总是在两个之一 状态:能够接受更多的输入,或者能够产生更多的输出。

我之前在设置解码器输入和输出流时没有出错,所以我认为流应该是好的。而且我在发送输入媒体时也没有收到任何警告,所以我认为我应该处于有效状态。但这种行为似乎与我认为文档所暗示的不符。如果感兴趣的话,我也只有 1 个输入和 1 个输出流。

那么这怎么会发生呢?我有来自该工具的相互矛盾的信息。数据准备好了,但我读错了,还是发生了其他事情?

编辑:

有几个 cmets 要求提供更多信息,一个要求提供最低限度的完整示例,因此我决定尝试一下。下面是一个小型 c 程序,它运行我运行的所有东西,它通过从文件读取输入并以与获取数据相同的方式发送它来模拟我的环境。我已经删除了几乎所有的错误处理,删除了辅助函数并硬编码了一些东西。该程序重现了该问题。我在 Visual Studio 2015 中运行它。

#include <stdlib.h>

//windows media foundation test
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <stdio.h>
#include <mferror.h>

int chunk_handler(unsigned char* pBuf, unsigned short length);

IMFTransform *pDecoder = NULL;
DWORD dwInputStreamID;
DWORD dwOutputStreamID;


// inspierd by FindDecoder here: https://msdn.microsoft.com/en-us/library/windows/desktop/ms701774(v=vs.85).aspx
HRESULT FindDecoder(
    IMFTransform **ppDecoder    // Receives a pointer to the decoder.
)

    HRESULT hr = S_OK;
    UINT32 count = 0;

    IMFActivate **ppActivate = NULL;

    MFT_REGISTER_TYPE_INFO inputType =  0 ;

    inputType.guidMajorType = MFMediaType_Audio;
    inputType.guidSubtype = MFAudioFormat_AAC;

    hr = MFTEnumEx(
        MFT_CATEGORY_AUDIO_DECODER,
        MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT | MFT_ENUM_FLAG_SORTANDFILTER,
        &inputType,      // Input type
        NULL,       // Output type
        &ppActivate,
        &count
    );

    if (SUCCEEDED(hr) && count == 0)
    
        hr = MF_E_TOPO_CODEC_NOT_FOUND;
    

    // Create the first decoder in the list.

    if (SUCCEEDED(hr))
    
        hr = ppActivate[0]->lpVtbl->ActivateObject(ppActivate[0], &IID_IMFTransform, (IUnknown**)ppDecoder);
    

    for (UINT32 i = 0; i < count; i++)
    
        ppActivate[i]->lpVtbl->Release(ppActivate[i]);
    
    CoTaskMemFree(ppActivate);

    return hr;


int main()

    UINT32 samplesPerSec = 44100;
    UINT32 bitsPerSample = 16;
    UINT32 cChannels = 2;

    HRESULT hr = S_OK;
    HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

    hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

    DWORD dwFlags = MFSTARTUP_FULL;
    hr = MFStartup(MF_VERSION, dwFlags);

    // Create decoder
    pDecoder = NULL;
    hr = FindDecoder(&pDecoder);

    // Create input and output audio types
    IMFMediaType *pMediaIn = NULL;        // Pointer to an encoded audio type.
    IMFMediaType *pMediaOut = NULL;       // Receives a matching PCM audio type.

    /* Create pMediaIn */

    // Calculate derived values.
    UINT32 blockAlign = cChannels * (bitsPerSample / 8);
    UINT32 bytesPerSecond = blockAlign * samplesPerSec;

    // Create the empty media type.
    hr = MFCreateMediaType(&pMediaIn);

    // Set attributes on the type.
    hr = pMediaIn->lpVtbl->SetGUID(pMediaIn, &MF_MT_MAJOR_TYPE, &MFMediaType_Audio);
    hr = pMediaIn->lpVtbl->SetGUID(pMediaIn, &MF_MT_SUBTYPE, &MFAudioFormat_AAC);
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION, 0x2a); // value can be found in my onenote, (AAC Profile, Level 4)
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AAC_PAYLOAD_TYPE, 3); // (LOAS/LATM)
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AUDIO_NUM_CHANNELS, cChannels);
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AUDIO_SAMPLES_PER_SECOND, samplesPerSec);

    // blockAlign, bytesPerSecond and independent samples were commented out previously
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AUDIO_BLOCK_ALIGNMENT, blockAlign);
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerSecond);
    hr = pMediaIn->lpVtbl->SetUINT32(pMediaIn, &MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);


    //first 12 bytes:
    //wPayloadType  = 0 (raw AAC)
    //wAudioProfileLevelIndication  = 0x29 (AAC Profile, Level 2)
    //wStructType  = 0
    //  The last two bytes of MF_MT_USER_DATA contain the value of AudiospecificConfig(), as defined by MPEG - 4.
    // 00010 0100 0010 000
    //AudioSpecificConfig.audioObjectType = 2 (AAC LC) (5 bits)
    //AudioSpecificConfig.samplingFrequencyIndex = 4 (4 bits) (44100hz)
    //AudioSpecificConfig.channelConfiguration = 2 (4 bits)
    //GASpecificConfig.frameLengthFlag = 0 (1 bit)
    //GASpecificConfig.dependsOnCoreCoder = 0 (1 bit)
    //GASpecificConfig.extensionFlag = 0 (1 bit)
    UINT8 audioSpecificConfig[] =  0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x10 ; 
    hr = pMediaIn->lpVtbl->SetBlob(pMediaIn, &MF_MT_USER_DATA, audioSpecificConfig, 14);


    /* Create pMediaOut */

    // Create the empty media type.
    hr = MFCreateMediaType(&pMediaOut);

    // Set attributes on the type.
    hr = pMediaOut->lpVtbl->SetGUID(pMediaOut, &MF_MT_MAJOR_TYPE, &MFMediaType_Audio);
    hr = pMediaOut->lpVtbl->SetGUID(pMediaOut, &MF_MT_SUBTYPE, &MFAudioFormat_PCM);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_AUDIO_NUM_CHANNELS, cChannels);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_AUDIO_SAMPLES_PER_SECOND, samplesPerSec);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_AUDIO_BLOCK_ALIGNMENT, blockAlign);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerSecond);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
    hr = pMediaOut->lpVtbl->SetUINT32(pMediaOut, &MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);

    // Figure out streamcounts and ID's
    DWORD inputStreamCount = 0;
    DWORD outputStreamCount = 0;

    hr = pDecoder->lpVtbl->GetStreamCount(pDecoder, &inputStreamCount, &outputStreamCount); // both StreamCounts == 1

    DWORD dwInputID[1] =  0 ; //hardcoded
    DWORD dwOutputID[1] =  0 ; //hardcoded

    hr = pDecoder->lpVtbl->GetStreamIDs(pDecoder, inputStreamCount, dwInputID, outputStreamCount, dwOutputID);
    if (FAILED(hr)) 
        if (hr == E_NOTIMPL) 
            // This is expected and quite ok. 
        
    

    dwInputStreamID = dwInputID[0];
    dwOutputStreamID = dwOutputID[0];

    // configure decoder for the two audio types
    hr = pDecoder->lpVtbl->SetInputType(pDecoder, dwInputStreamID, pMediaIn, 0);
    dwFlags = 0;
    hr = pDecoder->lpVtbl->SetOutputType(pDecoder, dwOutputStreamID, pMediaOut, dwFlags);

    /*one time setup is now done.*/

    // simulate sending in the first couple of chunks that I can get while trying to decode audio

    // Reading this from file, again this is just read from a file in this example, in my real application I get the data sent to me in audio frame chunks.
    // For example the first "chunk" of data is:
    //  47fc 0000 b090 8003 0020 2066 0001 9800 0de1 2000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 001c 

    errno_t err;
    FILE *file = NULL;
    fopen_s(&file, "input.txt", "rb");

    unsigned char line[10000]; //big enough
#define NR_OF_INPUTS 14
    //                           0   1   2   3   4    5    6    7    8    9   10   11   12   13
    int sizes[NR_OF_INPUTS] =  42, 42, 42, 42, 42, 340, 708, 503, 477, 493, 499, 448, 640, 511; // lengths of the data

    int i, j;
    for (i = 0; i < NR_OF_INPUTS; i++) 
        fread(line, sizeof(char), sizes[i], file);

        printf("Input chunk number: %d\n", i);
        for (j = 0; j < sizes[i]; j++) 
            printf(" %02x", line[j]);
        
        printf("\n\n");

        chunk_handler(line, sizes[i]);
    

    fclose(file);

    return 0;


int chunk_handler(unsigned char* pBuf, unsigned short length) 

    const UINT SamplesPerSecond = 44100;
    const UINT ChannelCount = 2;
    const UINT SampleCount = length * ChannelCount;
    const UINT BitsPerSample = 16;
    const UINT BufferLength = BitsPerSample / 8 * ChannelCount * length;
    const LONGLONG sampleDuration = (long long)length * (long long)10000000 / SamplesPerSecond; // in hns (hecto nano second?) 0.000 000 1. (Duration of the sample, in 100-nanosecond units., see IMFSample)

    HRESULT hr = S_OK;
    DWORD dwFlags = 0;

    /* Setup for processInput */

    IMFSample *pSample = NULL;
    IMFMediaBuffer *pInputBuffer = NULL;
    hr = MFCreateMemoryBuffer(
        length,   // Amount of memory to allocate, in bytes.
        &pInputBuffer
    );
    BYTE *pData = NULL;

    hr = pInputBuffer->lpVtbl->Lock(pInputBuffer, &pData, NULL, NULL);
    memcpy_s(pData, length, pBuf, length);
    hr = pInputBuffer->lpVtbl->SetCurrentLength(pInputBuffer, length);
    hr = pInputBuffer->lpVtbl->Unlock(pInputBuffer);


    hr = MFCreateSample(&pSample);
    hr = pSample->lpVtbl->AddBuffer(pSample, pInputBuffer);
    //hr = pSample->lpVtbl->SetUINT32(pSample, &MFSampleExtension_Discontinuity, TRUE);


    /* Setup for processOutput */
#define SAMPLES_PER_BUFFER 1
    MFT_OUTPUT_DATA_BUFFER pOutputSamples[SAMPLES_PER_BUFFER];
    MFT_OUTPUT_STREAM_INFO streamInfo =  0,0,0 ;
    MFT_OUTPUT_STREAM_INFO *pStreamInfo = &streamInfo;
    DWORD pdwStatus = 0;

    hr = pDecoder->lpVtbl->GetOutputStreamInfo(pDecoder, dwOutputStreamID, pStreamInfo);
    IMFSample *pOutSample = NULL;
    DWORD minimumSizeOfBuffer = pStreamInfo->cbSize;
    IMFMediaBuffer *pOutputBuffer = NULL;

    // code checking for if MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLE and such, Turns out client (me) ne4ed to provide sample so lets do that

    // Create the media buffer.
    hr = MFCreateMemoryBuffer(
        minimumSizeOfBuffer,   // Amount of memory to allocate, in bytes.
        &pOutputBuffer
    );
    hr = MFCreateSample(&pOutSample);
    hr = pOutSample->lpVtbl->AddBuffer(pOutSample, pOutputBuffer);

    pOutputSamples[0].pSample = pOutSample;
    pOutputSamples[0].dwStreamID = dwOutputStreamID;
    pOutputSamples[0].dwStatus = 0;
    pOutputSamples[0].pEvents = NULL;

    //INPUT
    hr = pDecoder->lpVtbl->ProcessInput(pDecoder, dwInputStreamID, pSample, dwFlags);
    if (FAILED(hr)) 
        if (hr == MF_E_NOTACCEPTING) 
            printf("Input cannot take more data\n");
        
        printf("error in ProcessInput\n");
    

    //OUTPUT
    hr = pDecoder->lpVtbl->ProcessOutput(pDecoder,
        dwFlags,
        SAMPLES_PER_BUFFER,
        pOutputSamples,
        &pdwStatus
    );
    if (FAILED(hr)) 
        if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) 
            // this is ok, just need to make more calls to ProcessInput
            printf("ProcessOutput needs more input\n");
        
        else 
            printf("error in ProcessOutput\n");
        
    
    return 0;

代码中的“file.txt”引用应包含以十六进制插入的以下内容:

47FC0000B090800300202066000198000DE120000000000000000000000000000000000000000000001C47FC0000B090800300202066000198000DE120000000000000000000000000000000000000000000001C47FC0000B090800300202066000198000DE120000000000000000000000000000000000000000000001C47FC0000B090800300202066000198000DE120000000000000000000000000000000000000000000001C47FC0000B090800300202066200198800DE120000000000000000000000000000000000000000000001C47FC0000B090800300FF4A214DE73987D722230566AD966E80B72A99797BFFB7978EB3DB9E248875BA38E42B7924EF58A2CCC578931AE67157BF6E7E3DC51B70265C888DA8CB0074753F9F0F3EF9B0F70CD8D5B2363C3B9CE1275DE0E4E7313C6F88FC611D87A932D4263BDFB8C74B5E9052DD5046AF66EA3AB55F9E2186ABCC5A72A3664F1CA21CC678AC24CBECD3797D7C2C50B335556E7DF5E51E5BECBEA1337CA71ACB7ACFD9EDABE47139CB0695F8D575EAB0E461BB5336ED00E2F0F5F381CAD2E9E2C0750FAE43460E9B372CCD016EE6E5547FF4F89EBACF3BE2EA21D7E9038E190CA4D46DB54633FCB9331E830DC6AFB7A1325F0E79DD1C6D886320F8C728DAA34AD00CA999CC089702E7CD7FD8EDEB1FEFFAB63F8D9E798CD483BE697B314BF749F17967FE9F46688D45D8D6B926300840223B9AD2B7291B288518835BB2629B61D79EAB5760D16FBEA3B909D4E7F56747FC0000B090800300FFFFB4216BD505EE8504B0D098342810840D0AF6CCCD71DEADAD6EB2F7117B85CAE255CC5C39130EF0EA7A7F7565275CB6CCBB9E70843BA4847444976535677F95480D0AEC0D0A63F40F1BF7B645AB70307D0262A388BBB2DE0F90E12FF5B46A2EAEB1C8E85C50061833EF9324731B4F7D29580BE0FA0DFAB6AA78341FF25422B0A7039F64C34BA063763E54D489805ADE242371C3C865F17360ADFD3956E6A6AC18D8463879AC4FA70D0ADAC9C5E146F5BB6541F8ACB2EEA53D1EF35EB0ED715F7C4B9A1A072908A66CE92A76328E43010B3820EC9E255C388BC5DE3E02D1393EA9FC70CD44555DE9F6E419C20F804F0361AC702CFFBDD6DEC4BBF6C7736AA1F207CFFC9934E9DBF039FE469D1B2B50B4507E8F6326BA9BD42B71A1A7AE74D23FF1F89CDAF27D614D3C2F8AD767004AC2421E11D8B86B058DEF88EB9A9C8E8C35CF980D0A56664CF27385181987F7878D06AB6F7E46200764841CB0820195C383613579C8836BA8622C06DF3889D200773EDC28AF89E3656DEE0699E66DDB9E7070EFB96BB024F2B4155A9E7806649A8C00A383586956581A0602015141282C140B100CF358BE372EEF5B65E51ACDEB2EDC4A9ADAD00D10420408761E859304BCD8828E72EEEC4AD4219560E77946692F3C51EA9BE12AC6431DFF674CF94931D357B6EF90E912A3B9ED135C6D620EBF7D887A38CC78E67B543838ABA936E67848A2744AF716CB8955C1CE9FB4784271584E49DE019496B654C6B413D74AA09ACFDF68233F39F176931E10A093B0C47961BC72F87C31FC3EA92BF55438EB90345122D1DE3BA14653C459A53C0E5CA3D8ED1097DDD3D5DB39C645D5E07396C76131C03B329A3C98E65A19E63380B51105F21089088038309AE0E4A17C6D6D827D5D85FF804805BAFC4441580F7FE5872EC3DD9C9C00312022F67A4D40AF900D0A1A5427DA204E113C93931E9A0209F7CD2303102E9F674A3B2B706E47FC0000B090800300FFEA210BD5159E8F6982B1602C703386BC713BE1AA54E75B0BA9092E29726E03838C74AADE6D55F03227C9F0F84F90159F2C64D517A77D6F1D69258146F8A3397C3BBA296084B027A9E6581390F1A00E4A3A26E67944B2F09DDEF851344D3BF2DA8A8B11C1C00750A7B34A310D04481F02AE190D0A41F83BD088CE2ED2BE9FE1819099C7A22D5C6738C32DE20F2201D6A603A2082DA21D6A677E2E471044B72C64C6085C734BD1C8D9E1287E93A588C4BAFB075E0D0A3751BC5ED4009F7F7E2500A8394D0B3D0D0A18F9CA85996BB1651362C72A739DE23A9ECDCF6CE771C523B42CB0A698213E554967559681AB800574D36050440CA4D00E48AA71748E65F5CAF6BAF7B01A64803C453ED2093582B6236DDE26AB2B2BC500002A129D1A56FA19400000E22952C4AE3BA298876484D89D4A832819C4D733AF5E6BADCACAE37CA17624B936E2A77A0914FB393C13DB11C2EF9C7E039F0718E7FFA578FDD875ADE3B4F3FAB20CF08AC721C4182E60E5A2D4DBCDA1810F88D64ADDA7CAD3981ABD3AD8E114BD505EBD8D5D7CBC9967899697B38D411ABAE11B391718EE9BCFAB917196FCC2B910B9170351CBAEB97577CD6F1D3215B0E72EAC739176AEAD6009AEBF56B882BE32319DFDFD33D5F28BA0706BA273601740829B8BADCE4537BD6BEF9BE0F9D2CBE60884F9225109718E47FC0000B090800300FFCF210BD51DB18561A4C0D9683033CD5F8EB5EB55ABDC64ADD244B26A26489960D9D84969A878658210307F2D01CE29359116C0739E92EA87B35B0D0AD5F223F143BE0D5D6EB2B69CCACFDECB8C36B5D9B771E03295998B9A62498D887106EC5E918E0C30076F5ECE6403391363A62CB44CBB6EFFD74CAC0CF90D0A2ADDC490E2306145701D85B1A9CA12C258CB7D81532D99FA8DF3C7A1894357950D0A0C0B7CBDB8D297200CFAAD4AEBC92BBDEE087AA9BFF6A49C2B7A7BC66900EBD78F859330033EB9C009C649B8A0E99E774E3ACC0C01DED63CC2247BB5A3C857D2D50D73D92C5BD345ECB6E9A6FEC7000D0AC2F870CEA2AB20BE4057C9EF9922083AA2ED64350DB514A84A0405E3CE51B6FAE53316CBFA2C87B393C5CDAEFB9561FB12DEC4C75F3C69C876464D094EC255825E57DF5AED77158BAEE979A9AA4BBCE372507AA149F8A2108A682471F2BD48D3DDDA49E896AFEA79BBCBCB96E00759FB9E77B110866D32F8C3061C4655E056BF8FD5F0417D1BF296F735F0D682437F67D9BCC05D6E8BD6E725050C41133E9D9D3E76280444FC260D435C631302720B6EA53BE9E7B890405D4564310F7F2C48027535A15DF807777F1EEF7F57DC0BDBBB398004C1CC5D159991BDAB0373926719A000006A1C47FC0000B090800300FFDE210BD515B290C2A0C1944C741819E4E755755C632A537335ADAD2EF5544B5481F1B472C55D552D4982F5FF89A5E18B3853B70D0A6380EF43AED06FBE5510F067C8E66CC6E0568C614076560EDDA09A3150EC4091D6FFEC2D78A38EF6FD5D52D0DC9F2BD2F3DC2DF9FA413F130355403920A91E9D2FA1FBAF09A7C239059DF6D3A72B24E8B96340288C68DEB4D9D44E9D2DAB0D3F8BBF15CDA44D549DCF8345F55DC594A21615BD6128E17D700240980B9DE7FC2456E7B7D5A0999AAD8EEEFF8B970E78B5BC34559F98FBA90D9C28BE444F0D0A8DEC59EB7D38104926B66CAC98E8B6DD3E69A2BA899B77D4C002DAC600A5734B62A006A1886E511063F09E93ADF3800B8E1CEBC528B8630900AE86759E7D1F08DF9DE47E57E93EC05714FDAAC43FCF03E57A551D29094162A8506AA31021E738E784DB8DDF350E667536B3875554BD5609E7C846792B5B0D0AA9DCCACFB143A6AB0B087917695136C22505859DDFB9E2CF3FC73FBDBEE77C1E853B74A0C023755C2BBA329D4C1D0298AC33342083E328F7F6663D5403C12943925C1007AAD0789F0E429D909DE53DA00D0ADEE57C77A966388A04C096FE3D08E41189037D5C2F32D71C015B46F31DF3F1D6855556F7A9339D0D0ADB788178227218CEB505E03BB5C7EEAD6A0BC63CFF7E9F4F00DD1C47FC0000B090800300FFE6210BD50D9E936181B0A0AC270B1102C451019A943352D95553994D5EEEEA5EB55448342299BD823F916D8AE11EB398B2D5CEB34CFAD3B2818D3F9E30884B0D0AEC3A05147354117537040109F46A27CD52806C4279C1C3E9FD9F6620C3ABF2AD8702767FBE226C934BDCD0E2BBD40E4003136CB1A3CCFD951FB2D8E9DE5415B55384B798B48C32469B56B54506917A7627332D8C4FD7A85897697186EBFCEBBE26D3C6D94D107198D32BAC82FCD0B081BFF7B43C4D8CF9786BE3221B39DB69080052494AF6E6A310BB6D8A50A45AD99DC00D0AAD604E4169612A8E75F96CB437EBE93645816D58F03BF46FEA654056FBABAE703A4899000C90F7F748200D39BD4FF897B19092B0BA000066B80E8357461289899CAF12055C66FEBE9E118E572C4494A438208A0F45632A1022503349AC6F8BE32B19399249BBBA4EB46DAA258D847BADAF69EADE38B19AA09DBD707825ABFE12D1E74C88B545354477A140A7CF3E83A47350CCA0796ED91F102C2378BD91B2A8820D9C0430C8096CC6C007279AF9984B6EAAC420D41DC51656027437981BC26ADA054E2EC7F2E24AF461733B014002A18EEFB2635C38700D0A89F872EACE26AF3B019F86B15383BFC72075F52EBE1F0CE3BFF5E3AE75AE38C4CA402304400714037E466C2802534AF14B0600101370D000705BE2E047FC0000B090800300FFB6212BD4ED9E936182D0A08CB04353DEC47192B753BE0975256A5945A507ED211FBFABFBDFD8B7E5E14B401438D2ED27B9D8E7E30651A8224007FF029525ACBEC13E7B2D6503DC54CAA43A1E35D7D264D0DB50F3789F04142A8902C1D6E0ED0080C561CD098EA1EDED08A04A8EBA2993001374E57D54528315752B83C7116C3725458CB05078123B8D8F3CB6E0976FB8F0A914A051B66CE31EDF5153AD9A011948859002F1A81CA6529E33155B64EAE03D2599AB5A501F05A72EAB90002CC2FA2BAE2E929CC7659A8099E80D6B9E79CA3C6E40B8FC25B18C6D689E819DBB335A7405E539405ECEAA41395B6D315146F14C910D40EC95B768B86204D44640B5A806AAF52AC2DDD423CC4B4E108F5844E6235D051E16A18131548023200C44065F1F6B5E5DEB2F2B75BD5D59755ABBA55CB04027445D19A4858A1C48F732E11169DB549AEC4BE6E3910C01EB3DBF85B3585D7E4DCAD92DFBBCEF54298CE02321D79BA0DAB0010D88105387DFFF664000066AC6CCFABF22F7401899A064D249404E84C90B989B5405C6FB534842FA37DAAE4679C9B902EBA4675EEE5DADF46E00251781D8F6DC5D6BCE23F1FC2194128B1316D08805038047FC0000B090800300FFFF72214DDEFFFFFF3336A9B1A2D1C81B54A1DA62A80B8F112FA2FF1FF13779A9C4DDF41DBF58B878CF6840F504F3FED85D1608DE4BDA7D0748C2711E387AFD2C656A7C078531B2A9B2D5A7F07E8671F9E7E41B3ED4F031EE923A47C8FF0D0A94305B24B1CEEA67EC5A3A74EA9B6F7285B3C68E0FB587FE1A30DBFBD5170532508F2D095022535C81BAF3C8068A2E476984FA49121FB15851DE70AA3EDAC7A4F285AC068F1F39BC124BAB7BCBEDB59A0D0A7EAB05AEE759E63F48C64EEAB770833C3352FBCE1E417534B457F411155A5694BBA5D190B8C934CE6B5DF88DAB9777303994614BDDA9A50CBC15E0AFEC1F3DA00DEBBC25304658E57ACDCCFE743CCF2D97096C43ABBA3EFDA47676B38B3C7E308D54B45A3B8D8470BF98EFCED3C9D5A375EE4566E39C6807A0072BA6F9F19E8B25EFEA994AFFE67886B80DAFE0FBF9A26768E3D4EA63974BA55CB83B15CBBDD95E3D3007C2009000009AA31113136E40D46506CD162A80AF9893DB1AFDFFECABE78BE1BB807B20B5E81EF71CC44F05FBA36F78B60957374E4DA34C8A7A9236A3EF391BC15CD9ED7957AB2422E89444BCBAE7B93F9D6E23D8FA2DDEE93FAE64FB7FB824389C1F8096FA407C88C3081FE7CB3FEBCE03728C752131676EA6B105B4681D75368D4F245EE81EACF386076278C95924087B56FC0E2AD55F77A6A44B28A9B25AF286BEBD27226A18C2E2BA296FD959452A32E9231ACF2CEE5C050478175B83696AFAE0118C5984F3F724A329DCE25230C9ABA45FA9EADB6E64C57E9609F22B4D3E8651E60DDF127A6010A3320BA3D0BFA22D921572043923A011330D0A003A5A67F67D3F2FB39FFDFF0D0ACF6763F88F838C001F3CFD9D523FCDFB3B02F19000007047FC0000B090800300FFF2216BD4FDBE8D0E620840EC8BC78BDFD139EED7968BC6B495922D9438B20F89CE72499D90F50FD31ABA4B912523B81ABD5BC3F1AE5F9FF34E75F97E68E4876E1AE381C96FE987C9F366AB724F3C634CA9AA4A1E27758C5D5EA32DF47634CE4B8EAC1DA6522172C6BEF6B78B6C21A968325B87FA4B8C75DFC9AF7E3AD024A6D45DF651B19D99133869232C0CA24BEF0DA6CA2133CC0277D02C0FBD2104C9A58C22A45E6E0520C1A55DEE48AA9B600D0AEA6BEE37AE66ACB8DDE4E53C23D36D4448C397749D19F2B1E68428D0A7C57EB91A4860B12C4D69769DF1D4C69CD4D04F9D72934AB58E51072C4B09925BD7DA02259693C4C0FC25218F6A78216AA094DB7AA3619D6F9E696E7B25510006A6DC24FD5BDB1348F57C63CAB3DF759729524CEF7E40014C5637D7AE61000322198D20A41142EC4C45431504650433AAD667339FAB5F7DC6B22D2AB8D456EEA5B63493B898F6DF0F70DD916C86A6D462BB14C15C776CF8EDA49868E510773CC69988D806E1870859E0B344AB8F1B6C3BFE417BF7E9884F2128F5F4DC3B63E1238E6312D76F2E8E576057340BD2E2A4ADD6484457609AA80209CD4DBE126F9A509DD011A9FA7DBD617DCF84B948BB7B616CEFDBE094B829D525FBCA7B2582A52421BAA94AC8A8A667836D98C5D5711A92FFE8B8F97433278764D5D5D597F7BE5013C0CD605C1C47FC0000B090800300FFC2210BD50DBA0D618431602A200B04C40656A92FD4DF152AAAEB591245A242AAED06E8F0672C5946157169B9AB6ACB4F1866BA4137814BE13F05A2411098969DCFF61E40B4BE944463971C375920B2BEBF3A52F0E2D1869C9A888FA900465D2C74FC76F45CA4CE0D0A140D0A695E04899CC9250D0A8F80C42290A86140C5B248EC8EB7392B6E14215BF0C9CA0341490623A1391B0D914C92E7395009A2E15134A7125C2557891339CE05826194D5D4EDAC6B192FF34B8600071E6E14E38CD3852EEEB194F2D963C817DF43D0C0E0FA9128E525AF7A10B9257719C014437538E585FC4A8F51E58128CCBE659EF01E420D0A1A6AFDDE784FA351F1AFD5009DF4FB669871E7BA8C8A2CD84E5DC355583B2A7A9F0FD2DE041D1BA072F00292D30563A2D4802108215EDB8E3F4D6F52AEB76D622D5A45CBCCAE01AC8707447199D2B8193647757C0014E4BC4E8DD9D251E0755086332FE1E122E318533919E1171A85F17DEC4B7F30EA40005ECF1322EF9705A2DADB354EF0D71653C09BA5771CF866F6B4692BFB53A20D0A36ED3BD63B5397E1AB1C76A6066610535CEC5857AA165F8A3BDE3AFE62C14540A8AFF1CAFD7351D7638E0D0AFE577D1B1753015C98CF3AD1588056C07EF11C47FC0000B090800300FFFF14210BD51DD612C2A238A04A1032F86DAF9E39F6BEEF61256A2492AE5A492E507266D8A485B1C91050CF618CD9D24CE9422880B92E969794BD6EE21043A9B997E8E5D02CAD01D5EDFCD8B871220B37D4CE28394314B723411472F6DA23E15CA10D66D7147D38C0B31AF672573349490915B6D97078FB5216D535437AFA64A678C4507414C17AE5E54217F0928AE120AA4785CF3F2AFE23CE722FEC6DAF8231ECE7B282285F116A70137C711FA2C90995AE22233874438CA69F0827AC2318B4DB7DD3825F4DCD72EFCA13BEECA7B33B500C24F86B032995348674CDE93D20D28DC69B44836A2268C6383600AD36D5EC1C7BEC2C0513E4629551BA966ADF93A8F5685BEEAF417384CBA8556AA4C9C797361D1FF6F65CAF91AA2A7180000D0AECFCF8A27CB9FCBB33CA6E75A45F9A5D5566D68ED32A7169E09938465DC13D5FF9F6F1C635A8AF3F648535B98D4154A08C4C15080982A2032FE3337AED5ED399CD5E5D719A8BAB9BE2173A6034F9A257E49449860F1F3234D1E7A7C1FA3154C6FC7C53088642EC3B6454AAE6AFB85BA6A3437CBE2624C9CDC9D46A9C00CAC1300557BF14621033ED6CCE4F846AB973EBF9F216D31DFC2A6205E3B65BF0EC84AFBF58E701778C6E78EF8004EE98EBEF8ADEFF856C00089D7AAD2B65E220AFE016A1473108E0CE9461F75AEAE24B9753B810A8C348A9870D7373F8FB3AFEE07C3521BFCCF7E51132B76548165BFAB62A73300FBF40003847FC0000B090800300FFFF1F210BD51DC6890761C0E84A281819D5E7377DB9F3795855CBA5DDB24BB9217C503B377F9A2BEC89E1CFC791C41158ED857519F6B98E872BBEA9B614500C6FCF7E468CB64F31AE089CDE696E45B29D9F3874F083E36F6AA0CA5F849622858B8F4153CB0721ABACB0FCF4B0A209E092CB70A10D0AC689F786783E85E022E8E02D4CC2AC1AE0984E701926788007936A163026CEC02DA74C159B3AB81003236120A2630E9E85980966AF95D712FAEBCA35EF9F4918972FF646A17B757FB28F2A67007C26B281C925832AE31703971AC6C931F10CD6DACABB61D49A8BF071B4947D952A33789A89A54C5757C1DF155B58C62BE42982A928E8ACE39C75DA8B0D2F0B518C7D975D846EC28236543C505AFB3D7ECA81A80000673F2EBE8DECEDFDFACAD8AB7AE33B43B2F05E722499D77A4D223A2DF7DC42437AC0B03D271F05379CE739CEA7B4B1A82C210D894C8452889828103BDB59BBBEF3BF699372A389556E1BB6AE495E3DBCE02863569A20106734826201CEDD6DBF328E8EA8E13833AFA5CE678A43ECACAE056F438204DE3F666ACC594CF22AC0A0238671CB13CA31BBEF5D1E38EC8CEFA372BA8EF29CB1EA825590EEC95BE0AB98BC4DFBFA60C7DFD18CA5FD6F1D94832B9D4A2F46EB40E777B2C321FC8BE45BBD477B3BFE94D7BFABB61DDDE62B2F679F007BB8ED7ACFD9D5C7D935F1E9E3FE9F1800AFEDF1FFAFE5F672FF3DE2FE50E7B83AFA400004B8275F20987F35768CCDAEE5D92358534EF57847FC0000B090800300FFE9210BD525A68D6181D098B4180B0E02C451308C20679BA6FCFBD73E655EF6B8B2692A4B092F554152C8CC9205E29FA1F9D772C440C559DA53E5DFD1398ACB4BE64512620F63F0F92B20EEC15947AB0E2032125AE3D054070D0A7FF21BED4335F82519E41E12FF8F0051C5B625EFCA1005A8AEBEFF8DE407E4DB9655E7ECB95ED8C7CB496524916480007916D9278CC3EBC697721DFF0D0869C9A008D1326C2993079D09DCB1D3D0AB84EF3ED249260EDBB1F2C37F7F6D7A3BBB6E69EF7291A667BAAAD6B918D4A92544E23A89B38AA35E781A4BEA47FAEFD482A873DD4D7CE903BB5C9B7826170537DD2C9D293450B2E49701CCE49E9CFE1DDF8D98C4700B9808559CD6F7BE9E000D01A4B1F4FA8B8787C3FCF282FAD6CCDC960542EB9CF77D5D75DC63F7AF27A51920A741D9A156250D0A115402408880CEA5CBBF9DEFCDE71CF3347189C52AEB8CB2BE302116B6FAA464C548A09BF3AC27F9AC0093C55F0004D671C4A53924BB758CF48C9F302A31E1B0E340339630896052D4E6D2C418E38203E952003C4B7F9635A743DE4D2C284D206EA7E25CEB9AAAD095F1306F11D7F2FE5323ABBE05EFBE031D2D467BF711A847074702B60B910C639FB30BC600066000DF8F6856EEE57D3C2B35BDCEBBFAD20365E9598E8854AB6361A0790B5FE6DB3F2DA27493AE106C01C0

输入文件也在这里: http://filebin.ca/3DACc6lkf882/input.txt

编辑 2:

这是来自 MFTrace 的示例,其中包含一些额外的跟踪:

23816,3D70 10:43:31.09829 CKernel32ExportDetours::OutputDebugStringA @ ----------------------------------ProcessInput----------------------------------
23816,3D70 10:43:31.09832 CMFTransformDetours::ProcessInput @00000229F1932698 Stream ID 0, Sample @00000229F19D2160, Time 0ms, Duration 0ms, Buffers 1, Size 640B,
23816,3D70 10:43:31.09832 CMFTransformDetours::ProcessMessage @00000229F1932698 Message type=0x10000000 MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, param=00000000
23816,3D70 10:43:31.09832 CKernel32ExportDetours::OutputDebugStringA @ GetOutputStatus says MFT_OUTPUT_STATUS_SAMPLE_READY
23816,3D70 10:43:31.09833 CKernel32ExportDetours::OutputDebugStringA @ GetInputStatus says does NOT accept data
23816,3D70 10:43:31.09834 CKernel32ExportDetours::OutputDebugStringA @ ----------------------------------ProcessOutput----------------------------------
23816,3D70 10:43:31.09834 CMFTransformDetours::ProcessMessage @00000229F1932698 Message type=0x10000000 MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, param=00000000
23816,3D70 10:43:31.09835 CMFTransformDetours::ProcessOutput @00000229F1932698 failed hr=0xC00D6D72 MF_E_TRANSFORM_NEED_MORE_INPUT
23816,3D70 10:43:31.09835 CKernel32ExportDetours::OutputDebugStringA @ ProcessOutput needs more input
23816,3D70 10:43:31.09836 CKernel32ExportDetours::OutputDebugStringA @ GetInputStatus says MFT_INPUT_STATUS_ACCEPT_DATA

【问题讨论】:

什么是解码器?这些也需要写入规范,如果它们进入无效状态,那么整个转换也是如此。 IE。你可能有一个错误的解码器。 @Olaf 您将其作为 C++ 的依据是什么?这是纯 C。 就解决方法而言,只需将GetInputStatus 视为建议。无论如何,这是一个可选函数(即E_NOTIMPL 是一个有效的返回码),所以如果你不想调用它,你可以不调用它。 @Olaf,COM 定义了 C 和 C++ 绑定。 C 没有接口和方法的概念,但 COM 有。使用“方法”一词来指代 COM 接口上的方法并没有错,即使您使用 C 绑定调用它也是如此。 也就是说,我猜你根本没有向它传递有效的 AAC 数据,因此它认为它正处于某个需要更多数据才能完成的操作的中间。正如我所说的原始“什么是解码器”。这可能与特定解码器及其行为方式有关,与 API 本身无关。 【参考方案1】:

正如 cmets 中所述,在花费了一些时间处理您的代码之后,我能够重现您报告的行为。但是,当我使用任意文件进行输入时也是如此。诚然,我很惊讶地看到解码器报告了MFT_OUTPUT_STATUS_SAMPLE_READY 的状态。因此,我同意 Ben 关于有效 AAC 数据的观点。

也许您可以简单地使用IMFSourceReader 为您提供示例,而不是手动解析示例,这看起来类似于以下内容:

为了简洁省略错误检查

HRESULT configure_reader(IMFSourceReader *reader, IMFMediaType **resultingMediaType)

    HRESULT hr = S_OK;
    IMFMediaType *pcmMediaType = NULL;
    IMFMediaType *partialType = NULL;

    // Create a partial media type that specifies uncompressed PCM audio.
    hr = MFCreateMediaType(&partialType);
    hr = partialType->lpVtbl->SetGUID(partialType, &MF_MT_MAJOR_TYPE, &MFMediaType_Audio);
    hr = partialType->lpVtbl->SetGUID(partialType, &MF_MT_SUBTYPE, &MFAudioFormat_PCM);

    // Set this type on the source reader. The source reader will load the necessary decoder.
    hr = reader->lpVtbl->SetCurrentMediaType(reader,
                                             (DWORD) MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                             NULL,
                                             partialType);

    // Get the complete uncompressed format.
    hr = reader->lpVtbl->GetCurrentMediaType(reader,
                                             (DWORD) MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                             &pcmMediaType);

    // Ensure the stream is selected.
    hr = reader->lpVtbl->SetStreamSelection(reader,
                                            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                            TRUE);

    if (resultingMediaType)
    
        *resultingMediaType = pcmMediaType;
        (*resultingMediaType)->lpVtbl->AddRef(*resultingMediaType);
    

    (void) pcmMediaType->lpVtbl->Release(pcmMediaType);
    (void) partialType->lpVtbl->Release(partialType);

    return hr;


// .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

void process_aac_audio()

    HRESULT hr = S_OK;
    IMFSourceReader *reader = NULL;
    hr = MFCreateSourceReaderFromURL(L"input.aac",
                                     NULL,
                                     &reader);

    IMFMediaType *pcm_media_type = NULL;
    hr = configure_reader(reader, &pcm_media_type);
    assert(pcm_media_type);

    DWORD total_bytes = 0;
    DWORD buffer_length = 0;
    BYTE *audioData = NULL;

    IMFSample *sample = NULL;
    IMFMediaBuffer *mediaBuffer = NULL;

    while (1)
    
        DWORD dwFlags = 0;
        hr = reader->lpVtbl->ReadSample(reader,
                                        (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
                                        0,
                                        NULL,
                                        &dwFlags,
                                        NULL,
                                        &sample);

        if (FAILED(hr))  break; 
        if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
        
            printf("Type change \n");
            // TODO:
        

        if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM)
        
            printf("End of input file. \n");
            break;
        

        if (sample == NULL)
        
            printf("No sample \n");
            continue;
        

        hr = sample->lpVtbl->ConvertToContiguousBuffer(sample, &mediaBuffer);
        hr = mediaBuffer->lpVtbl->Lock(mediaBuffer, &audioData, NULL, &buffer_length);
        // TODO: process buffer
        hr = mediaBuffer->lpVtbl->Unlock(mediaBuffer);
        audioData = NULL;

        total_bytes += buffer_length; // <-- update running total
        (void) sample->lpVtbl->Release(sample);
        (void) mediaBuffer->lpVtbl->Release(mediaBuffer);
    

    if (sample)
        (void) sample->lpVtbl->Release(sample);

    if (mediaBuffer)
        (void) mediaBuffer->lpVtbl->Release(mediaBuffer);

在这种情况下,解码器已加载并用于您直接提供 PCM 样本。您提供的文件 (input.txt) 失败,但其他有效的 aac 文件工作正常。

您可能正在处理流而不是文件。在这种情况下,使用类似于以下的代码创建源代码:

HRESULT create_media_source(IMFByteStream *byteStream, IMFMediaSource **ppSource)

    MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID;
    IMFSourceResolver* sourceResolver = NULL;
    IUnknown* source = NULL;

    HRESULT hr = MFCreateSourceResolver(&sourceResolver);
    hr = sourceResolver->lpVtbl->CreateObjectFromByteStream(sourceResolver,
                                                            byteStream,
                                                            NULL, // URL
                                                            MF_RESOLUTION_MEDIASOURCE,
                                                            NULL, // IPropertyStore
                                                            &objectType,
                                                            &source);

    // get the IMFMediaSource interface from the media source.
    hr = source->lpVtbl->QueryInterface(source, &IID_IMFMediaSource, ppSource);
    (void) sourceResolver->lpVtbl->Release(sourceResolver);
    (void) source->lpVtbl->Release(source);
    return hr;

但是,请注意CreateObjectFromByteStream 需要一种方法来识别内容类型。您可以在字节流上实现 IMFAttributes [特别是 GetAllocatedString] 或传入 URL

【讨论】:

谢谢@Jeff!用任意数据进行测试真是太好了。我得到了同样的结果。因此,在这种情况下似乎无法信任 GetOutputStatus。即使有错误检查hr 也不过是S_OK 的未初始化数据。所以我想我不能相信 MF 会告诉我我的输入数据是否有任何问题。 @Ben 关于 AAC 数据有问题可能是正确的。很难将数据与其他来源进行比较,因为我需要找到正确的 LATM AAC 数据并能够从文件中提取有趣的位(即删除文件头)。 源不是文件,但我可以将输入的内容写入流。会试试的。但是为什么在这种情况下流更好呢?我猜我做错事的机会少了?或者在这方面使用流或 SourceReader 还有其他固有优势吗? 标记为已接受的答案,因为您可以证明 GetOutputStatus 即使对于明显错误的输入也会产生 MFT_OUTPUT_STATUS_SAMPLE_READY,因此它不应该被信任(至少在这种情况下)并且这个问题很可能成为我的输入数据。 @AlexTelon - 流阅读器是源阅读器。我把它包括在内是因为我认为你可能需要它来代替文件。但是,在使用源阅读器之前,您可能希望继续尝试使用有效数据的当前解决方案。这可能是最简单的,但由于误导性状态有点困难。 @AlexTelon - 我有个主意。使用我提供的示例(基于文件)来查看交付缓冲区中的数据,并使用它来创建示例以提供您的原始解决方案怎么样?解决方案正常运行后,您就可以摆脱源阅读器了。

以上是关于GetOutputStatus 表示输出已准备就绪,ProcessOutput 表示 NEED_MORE_INPUT的主要内容,如果未能解决你的问题,请参考以下文章

检测 FB 共享对话框已准备就绪

在将内容附加到页面后,我如何知道 DOM 已准备就绪?

如何在没有框架的情况下检查 DOM 是不是准备就绪?

描述符就绪条件

状态集为啥必须无头服务

进程的状态与转换